@crabcloud/cli 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crab.js +2351 -188
- package/dist/skills/crabcloud-hr/SKILL.md +60 -0
- package/dist/skills/crabcloud-shop/SKILL.md +59 -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,298 @@ 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 shopSpecGroupCreateInput = object({
|
|
6095
|
+
name: string2().trim().min(1).max(30),
|
|
6096
|
+
values: array(string2().trim().min(1).max(60)).min(1).max(20)
|
|
6097
|
+
});
|
|
6098
|
+
var shopSpecGroupUpdateInput = shopSpecGroupCreateInput.partial();
|
|
6099
|
+
var comboValues = array(string2().min(1).max(60)).min(1).max(24);
|
|
6100
|
+
var shopStoreCreateInput = object({
|
|
6101
|
+
name: string2().min(1).max(60),
|
|
6102
|
+
tagline: string2().max(200).default("")
|
|
6103
|
+
});
|
|
6104
|
+
var shopCategoryCreateInput = object({
|
|
6105
|
+
name: string2().min(1).max(60),
|
|
6106
|
+
parentId: string2().min(1).nullish()
|
|
6107
|
+
});
|
|
6108
|
+
var shopCategoryUpdateInput = object({
|
|
6109
|
+
name: string2().min(1).max(60),
|
|
6110
|
+
parentId: string2().min(1).nullable()
|
|
6111
|
+
}).partial();
|
|
6112
|
+
var shopVariantInput = object({
|
|
6113
|
+
combo: comboValues,
|
|
6114
|
+
priceCents: number2().int().min(0).max(1e8),
|
|
6115
|
+
stock: number2().int().min(0).max(1e7).nullish(),
|
|
6116
|
+
enabled: boolean2().default(true)
|
|
6117
|
+
});
|
|
6118
|
+
var shopProductCreateInput = object({
|
|
6119
|
+
kind: _enum(["digital", "physical"]),
|
|
6120
|
+
name: string2().min(1).max(140),
|
|
6121
|
+
description: string2().max(2000).default(""),
|
|
6122
|
+
categoryId: string2().min(1).nullish(),
|
|
6123
|
+
status: _enum(["draft", "active", "archived"]).default("draft"),
|
|
6124
|
+
priceCents: number2().int().min(0).max(1e8).nullish(),
|
|
6125
|
+
stock: number2().int().min(0).max(1e7).nullish(),
|
|
6126
|
+
deliveryType: _enum(["file", "license"]).nullish(),
|
|
6127
|
+
deliveryFileId: string2().min(1).nullish(),
|
|
6128
|
+
specAxes: array(object({ name: string2().min(1).max(30), values: comboValues })).max(3).nullish(),
|
|
6129
|
+
specGroupIds: array(string2().min(1)).max(3).nullish(),
|
|
6130
|
+
variants: array(shopVariantInput).max(24).nullish()
|
|
6131
|
+
});
|
|
6132
|
+
var shopProductUpdateInput = shopProductCreateInput.partial();
|
|
6133
|
+
var shopStockAdjustInput = object({
|
|
6134
|
+
mode: _enum(["in", "out", "set"]),
|
|
6135
|
+
amount: number2().int().min(0).max(1e7),
|
|
6136
|
+
variantId: string2().min(1).nullish(),
|
|
6137
|
+
reason: _enum(["purchase", "manual"]).nullish(),
|
|
6138
|
+
note: string2().max(200).nullish()
|
|
6139
|
+
});
|
|
6140
|
+
var shopStripeConnectInput = object({
|
|
6141
|
+
secretKey: string2().min(1).max(255).regex(/^(sk|rk)_(test|live)_/, "must be a Stripe secret key (sk_…/rk_…)"),
|
|
6142
|
+
webhookSecret: string2().max(255).regex(/^whsec_/, "must be a Stripe webhook signing secret (whsec_…)").optional()
|
|
6143
|
+
});
|
|
6144
|
+
var shopDiscountCodeCreateInput = object({
|
|
6145
|
+
code: string2().trim().regex(/^[A-Z0-9]{0,20}$/, "code must be uppercase letters/digits").optional(),
|
|
6146
|
+
kind: _enum(["amount_off", "percent_off"]),
|
|
6147
|
+
amountCents: number2().int().min(1).max(1e8).nullish(),
|
|
6148
|
+
percent: number2().int().min(1).max(99).nullish(),
|
|
6149
|
+
minSpendCents: number2().int().min(0).max(1e8).nullish(),
|
|
6150
|
+
maxUses: number2().int().min(1).max(1e6).nullish(),
|
|
6151
|
+
expiresAt: number2().int().min(0).nullish()
|
|
6152
|
+
});
|
|
6153
|
+
var shopDiscountCodeToggleInput = object({
|
|
6154
|
+
status: _enum(["active", "disabled"])
|
|
6155
|
+
});
|
|
6156
|
+
var shopCustomerAssignInput = object({
|
|
6157
|
+
memberId: string2().min(1).nullable()
|
|
6158
|
+
});
|
|
6159
|
+
var shopCustomerNoteInput = object({
|
|
6160
|
+
note: string2().max(2000)
|
|
6161
|
+
});
|
|
6162
|
+
var shopInviteResetInput = object({
|
|
6163
|
+
permissions: array(_enum(SHOP_PERMISSIONS)).default(["orders", "customers"]),
|
|
6164
|
+
bizRoles: array(_enum(["sales", "support"])).default([])
|
|
6165
|
+
});
|
|
6166
|
+
var shopMemberUpdateInput = object({
|
|
6167
|
+
permissions: array(_enum(SHOP_PERMISSIONS)).min(0).max(SHOP_PERMISSIONS.length),
|
|
6168
|
+
bizRoles: array(_enum(["sales", "support"])).nullish()
|
|
6169
|
+
});
|
|
6170
|
+
var shopStoreRoleCreateInput = object({
|
|
6171
|
+
name: string2().trim().min(1).max(30),
|
|
6172
|
+
permissions: array(_enum(SHOP_PERMISSIONS)).max(SHOP_PERMISSIONS.length)
|
|
6173
|
+
});
|
|
6174
|
+
var shopStoreRoleUpdateInput = object({
|
|
6175
|
+
name: string2().trim().min(1).max(30).optional(),
|
|
6176
|
+
permissions: array(_enum(SHOP_PERMISSIONS)).max(SHOP_PERMISSIONS.length).optional()
|
|
6177
|
+
});
|
|
6178
|
+
var shopShippingAddressInput = object({
|
|
6179
|
+
name: string2().min(1).max(80),
|
|
6180
|
+
phone: string2().min(1).max(40),
|
|
6181
|
+
region: string2().max(120).optional(),
|
|
6182
|
+
detail: string2().min(1).max(500)
|
|
6183
|
+
});
|
|
6184
|
+
var shopCheckoutInput = object({
|
|
6185
|
+
productId: string2().min(1),
|
|
6186
|
+
variantId: string2().min(1).nullish(),
|
|
6187
|
+
quantity: number2().int().min(1).max(99).default(1),
|
|
6188
|
+
buyerEmail: string2().email().max(200),
|
|
6189
|
+
shippingAddress: shopShippingAddressInput.nullish(),
|
|
6190
|
+
discountCode: string2().trim().max(20).nullish(),
|
|
6191
|
+
provider: _enum(["auto", "mock", "stripe"]).default("auto")
|
|
6192
|
+
});
|
|
6193
|
+
var shopShipInput = object({
|
|
6194
|
+
trackingNumber: string2().min(1).max(120),
|
|
6195
|
+
quantity: number2().int().min(1).max(1e7).optional()
|
|
6196
|
+
});
|
|
6197
|
+
var shopApplicationCreateInput = object({
|
|
6198
|
+
name: string2().trim().min(1).max(60),
|
|
6199
|
+
tagline: string2().trim().max(200).default("")
|
|
6200
|
+
});
|
|
6201
|
+
// ../../packages/contracts/src/hr.ts
|
|
6202
|
+
var hrPermissionFlags = [
|
|
6203
|
+
"org.manage",
|
|
6204
|
+
"people.write",
|
|
6205
|
+
"tasks.write",
|
|
6206
|
+
"sensitive.read"
|
|
6207
|
+
];
|
|
6208
|
+
var hrEventTypes = [
|
|
6209
|
+
"onboard",
|
|
6210
|
+
"contract",
|
|
6211
|
+
"change",
|
|
6212
|
+
"attendance",
|
|
6213
|
+
"payroll",
|
|
6214
|
+
"award",
|
|
6215
|
+
"asset",
|
|
6216
|
+
"offboard",
|
|
6217
|
+
"note"
|
|
6218
|
+
];
|
|
6219
|
+
var hrTaskStatuses = ["alert", "confirm", "queued", "done"];
|
|
6220
|
+
var hrTaskDecisions = ["approved", "rejected"];
|
|
6221
|
+
var optionalDay = number2().int().positive().nullable().optional();
|
|
6222
|
+
var hrOrgCreateInput = object({
|
|
6223
|
+
name: string2().trim().min(1).max(80)
|
|
6224
|
+
});
|
|
6225
|
+
var hrOrgUpdateInput = object({
|
|
6226
|
+
name: string2().trim().min(1).max(80)
|
|
6227
|
+
});
|
|
6228
|
+
var hrMemberAddInput = object({
|
|
6229
|
+
username: string2().trim().toLowerCase().min(3).max(32),
|
|
6230
|
+
title: string2().trim().max(40).optional()
|
|
6231
|
+
});
|
|
6232
|
+
var hrMemberUpdateInput = object({
|
|
6233
|
+
role: _enum(["admin", "member"]).optional(),
|
|
6234
|
+
permissions: record(_enum(hrPermissionFlags), boolean2()).optional(),
|
|
6235
|
+
title: string2().trim().max(40).optional()
|
|
6236
|
+
});
|
|
6237
|
+
var hrPersonCreateInput = object({
|
|
6238
|
+
name: string2().trim().min(1).max(60),
|
|
6239
|
+
employeeNo: string2().trim().max(40).optional(),
|
|
6240
|
+
dept: string2().trim().max(40).optional(),
|
|
6241
|
+
role: string2().trim().max(40).optional(),
|
|
6242
|
+
status: _enum(["active", "probation"]).optional(),
|
|
6243
|
+
hiredAt: number2().int().positive(),
|
|
6244
|
+
probationEnd: optionalDay,
|
|
6245
|
+
contractType: string2().trim().max(20).optional(),
|
|
6246
|
+
contractStart: optionalDay,
|
|
6247
|
+
contractEnd: optionalDay,
|
|
6248
|
+
mobile: string2().trim().max(20).optional(),
|
|
6249
|
+
email: string2().trim().max(120).optional(),
|
|
6250
|
+
profile: record(string2(), unknown()).optional(),
|
|
6251
|
+
idNumber: string2().trim().max(18).optional(),
|
|
6252
|
+
salaryCents: number2().int().min(0).max(1e7).nullable().optional(),
|
|
6253
|
+
notes: string2().trim().max(2000).optional()
|
|
6254
|
+
});
|
|
6255
|
+
var hrImportRowInput = hrPersonCreateInput.omit({ status: true });
|
|
6256
|
+
var hrImportInput = object({
|
|
6257
|
+
rows: array(hrImportRowInput).min(1).max(200)
|
|
6258
|
+
});
|
|
6259
|
+
var hrPersonUpdateInput = object({
|
|
6260
|
+
name: string2().trim().min(1).max(60).optional(),
|
|
6261
|
+
employeeNo: string2().trim().max(40).optional(),
|
|
6262
|
+
dept: string2().trim().max(40).optional(),
|
|
6263
|
+
role: string2().trim().max(40).optional(),
|
|
6264
|
+
status: _enum(["active", "probation", "resigned"]).optional(),
|
|
6265
|
+
hiredAt: number2().int().positive().optional(),
|
|
6266
|
+
probationEnd: optionalDay,
|
|
6267
|
+
contractType: string2().trim().max(20).optional(),
|
|
6268
|
+
contractStart: optionalDay,
|
|
6269
|
+
contractEnd: optionalDay,
|
|
6270
|
+
offboardedAt: optionalDay,
|
|
6271
|
+
mobile: string2().trim().max(20).optional(),
|
|
6272
|
+
email: string2().trim().max(120).optional(),
|
|
6273
|
+
profile: record(string2(), unknown()).optional(),
|
|
6274
|
+
idNumber: string2().trim().max(18).nullable().optional(),
|
|
6275
|
+
salaryCents: number2().int().min(0).max(1e7).nullable().optional(),
|
|
6276
|
+
notes: string2().trim().max(2000).optional()
|
|
6277
|
+
});
|
|
6278
|
+
var hrPersonListQuery = object({
|
|
6279
|
+
q: string2().trim().max(100).optional(),
|
|
6280
|
+
status: _enum(["active", "probation", "resigned"]).optional(),
|
|
6281
|
+
dept: string2().trim().max(40).optional(),
|
|
6282
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6283
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6284
|
+
});
|
|
6285
|
+
var hrEventCreateInput = object({
|
|
6286
|
+
type: _enum(hrEventTypes),
|
|
6287
|
+
note: string2().trim().min(1).max(2000),
|
|
6288
|
+
at: number2().int().positive().optional()
|
|
6289
|
+
});
|
|
6290
|
+
var hrEventListQuery = object({
|
|
6291
|
+
personId: string2().trim().min(1).max(64).optional(),
|
|
6292
|
+
type: _enum(hrEventTypes).optional(),
|
|
6293
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6294
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6295
|
+
});
|
|
6296
|
+
var hrTaskCreateInput = object({
|
|
6297
|
+
kind: string2().trim().min(1).max(40),
|
|
6298
|
+
title: string2().trim().min(1).max(120),
|
|
6299
|
+
node: string2().trim().max(200).optional(),
|
|
6300
|
+
detail: string2().trim().max(2000).optional(),
|
|
6301
|
+
status: _enum(hrTaskStatuses).optional(),
|
|
6302
|
+
dueAt: number2().int().positive().nullable().optional(),
|
|
6303
|
+
assigneeAccountId: string2().trim().max(64).nullable().optional(),
|
|
6304
|
+
source: string2().trim().max(20).optional(),
|
|
6305
|
+
refType: string2().trim().max(20).optional(),
|
|
6306
|
+
refId: string2().trim().max(64).optional(),
|
|
6307
|
+
meta: record(string2(), unknown()).nullable().optional()
|
|
6308
|
+
});
|
|
6309
|
+
var hrTaskUpdateInput = object({
|
|
6310
|
+
status: _enum(hrTaskStatuses).optional(),
|
|
6311
|
+
node: string2().trim().max(200).optional(),
|
|
6312
|
+
detail: string2().trim().max(2000).optional(),
|
|
6313
|
+
dueAt: number2().int().positive().nullable().optional(),
|
|
6314
|
+
assigneeAccountId: string2().trim().max(64).nullable().optional(),
|
|
6315
|
+
decision: _enum(hrTaskDecisions).optional(),
|
|
6316
|
+
decisionNote: string2().trim().max(500).optional(),
|
|
6317
|
+
meta: record(string2(), unknown()).nullable().optional()
|
|
6318
|
+
});
|
|
6319
|
+
var hrTaskListQuery = object({
|
|
6320
|
+
status: _enum(hrTaskStatuses).optional(),
|
|
6321
|
+
kind: string2().trim().max(40).optional(),
|
|
6322
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6323
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6324
|
+
});
|
|
6325
|
+
var hrAnnouncementCreateInput = object({
|
|
6326
|
+
title: string2().trim().min(1).max(120),
|
|
6327
|
+
body: string2().trim().min(1).max(4000)
|
|
6328
|
+
});
|
|
6329
|
+
var hrAnnouncementListQuery = object({
|
|
6330
|
+
includeWithdrawn: _enum(["1", "0"]).optional(),
|
|
6331
|
+
limit: exports_coerce.number().int().min(1).max(100).optional(),
|
|
6332
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6333
|
+
});
|
|
6334
|
+
var hrDictTypes = ["dept", "position"];
|
|
6335
|
+
var hrDictCreateInput = object({
|
|
6336
|
+
type: _enum(hrDictTypes),
|
|
6337
|
+
name: string2().trim().min(1).max(40)
|
|
6338
|
+
});
|
|
6339
|
+
var hrAttachmentListQuery = object({
|
|
6340
|
+
personId: string2().trim().max(64).optional(),
|
|
6341
|
+
taskId: string2().trim().max(64).optional(),
|
|
6342
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6343
|
+
offset: exports_coerce.number().int().min(0).optional()
|
|
6344
|
+
});
|
|
6345
|
+
var optionalDayField = number2().int().positive().nullable().optional();
|
|
6346
|
+
var hrOnboardFormSchema = object({
|
|
6347
|
+
name: string2().trim().max(60).optional(),
|
|
6348
|
+
gender: string2().trim().max(20).optional(),
|
|
6349
|
+
birthDate: optionalDayField,
|
|
6350
|
+
mobile: string2().trim().max(20).optional(),
|
|
6351
|
+
email: string2().trim().max(120).optional(),
|
|
6352
|
+
idNumber: string2().trim().max(18).optional(),
|
|
6353
|
+
education: string2().trim().max(40).optional(),
|
|
6354
|
+
address: string2().trim().max(120).optional(),
|
|
6355
|
+
emergencyContact: string2().trim().max(120).optional(),
|
|
6356
|
+
notes: string2().trim().max(2000).optional(),
|
|
6357
|
+
employeeNo: string2().trim().max(40).optional(),
|
|
6358
|
+
dept: string2().trim().max(40).optional(),
|
|
6359
|
+
role: string2().trim().max(40).optional(),
|
|
6360
|
+
contractType: string2().trim().max(20).optional(),
|
|
6361
|
+
contractStart: optionalDayField,
|
|
6362
|
+
contractEnd: optionalDayField,
|
|
6363
|
+
employmentType: _enum(["probation", "active"]).optional(),
|
|
6364
|
+
probationEnd: optionalDayField,
|
|
6365
|
+
hiredAt: number2().int().positive().optional(),
|
|
6366
|
+
salaryCents: number2().int().min(0).max(1e7).nullable().optional()
|
|
6367
|
+
});
|
|
6368
|
+
var hrRegularizeFormSchema = object({
|
|
6369
|
+
selfSummary: string2().trim().max(4000).optional(),
|
|
6370
|
+
newRole: string2().trim().max(40).optional(),
|
|
6371
|
+
salaryCents: number2().int().min(0).max(1e7).nullable().optional(),
|
|
6372
|
+
probationReview: string2().trim().max(2000).optional(),
|
|
6373
|
+
regularizeDate: optionalDayField
|
|
6374
|
+
});
|
|
6375
|
+
var hrOffboardFormSchema = object({
|
|
6376
|
+
offboardType: _enum(["personal", "mutual", "dismissal"]).optional(),
|
|
6377
|
+
reason: string2().trim().max(2000).optional(),
|
|
6378
|
+
lastDay: optionalDayField,
|
|
6379
|
+
handoverNotes: string2().trim().max(2000).optional()
|
|
6380
|
+
});
|
|
6075
6381
|
// ../../packages/contracts/src/index.ts
|
|
6076
6382
|
var accountDomain = "crabcloud.cc";
|
|
6077
6383
|
var adminDomain = `admin.${accountDomain}`;
|
|
@@ -6094,7 +6400,10 @@ var scopes = [
|
|
|
6094
6400
|
"contacts.write",
|
|
6095
6401
|
"collab.read",
|
|
6096
6402
|
"collab.write",
|
|
6097
|
-
"
|
|
6403
|
+
"hr.read",
|
|
6404
|
+
"hr.write",
|
|
6405
|
+
"agent.read",
|
|
6406
|
+
"agent.write"
|
|
6098
6407
|
];
|
|
6099
6408
|
var scopeGroups = [
|
|
6100
6409
|
{ id: "account", scopes: ["account.read", "credits.read"] },
|
|
@@ -6102,7 +6411,8 @@ var scopeGroups = [
|
|
|
6102
6411
|
{ id: "storage", scopes: ["storage.read", "storage.write", "storage.delete"] },
|
|
6103
6412
|
{ id: "collab", scopes: ["collab.read", "collab.write"] },
|
|
6104
6413
|
{ id: "contacts", scopes: ["contacts.read", "contacts.write"] },
|
|
6105
|
-
{ id: "
|
|
6414
|
+
{ id: "hr", scopes: ["hr.read", "hr.write"] },
|
|
6415
|
+
{ id: "agent", scopes: ["agent.read", "agent.write"] }
|
|
6106
6416
|
];
|
|
6107
6417
|
function expandScopes(input) {
|
|
6108
6418
|
const expanded = input.flatMap((item) => {
|
|
@@ -6184,7 +6494,8 @@ var sessionSummarySchema = object({
|
|
|
6184
6494
|
var deviceStartInput = object({
|
|
6185
6495
|
name: string2().trim().min(1).max(80),
|
|
6186
6496
|
scopes: array(_enum(scopes)).min(1).max(scopes.length),
|
|
6187
|
-
space: string2().trim().min(1).max(80).optional()
|
|
6497
|
+
space: string2().trim().min(1).max(80).optional(),
|
|
6498
|
+
org: string2().trim().min(1).max(80).optional()
|
|
6188
6499
|
});
|
|
6189
6500
|
var devicePollInput = object({ deviceCode: string2().min(16).max(200) });
|
|
6190
6501
|
var deviceDecisionInput = object({
|
|
@@ -6197,11 +6508,18 @@ var agentTokenSummarySchema = object({
|
|
|
6197
6508
|
name: string2(),
|
|
6198
6509
|
scopes: array(_enum(scopes)),
|
|
6199
6510
|
collabSpace: string2().nullable().optional(),
|
|
6511
|
+
hrOrg: string2().nullable().optional(),
|
|
6512
|
+
kind: _enum(["agent", "api"]).optional(),
|
|
6513
|
+
keyHint: string2().optional(),
|
|
6200
6514
|
createdAt: number2(),
|
|
6201
6515
|
lastUsedAt: number2().nullable(),
|
|
6202
6516
|
expiresAt: number2().nullable(),
|
|
6203
6517
|
revoked: boolean2()
|
|
6204
6518
|
});
|
|
6519
|
+
var apiKeyCreateInput = object({
|
|
6520
|
+
name: string2().trim().min(1).max(64),
|
|
6521
|
+
scopes: array(string2().trim().min(1).max(40)).min(1).max(32)
|
|
6522
|
+
});
|
|
6205
6523
|
var oauthProviders = ["google", "reai", "github"];
|
|
6206
6524
|
var oauthUnbindInput = object({ provider: _enum(oauthProviders) });
|
|
6207
6525
|
var oauthOnboardingConfirmInput = object({
|
|
@@ -6248,6 +6566,12 @@ var adminPlanTierUpdateInput = object({
|
|
|
6248
6566
|
quotaJson: record(string2(), unknown()).optional(),
|
|
6249
6567
|
status: _enum(adminPlanTierStatuses).optional()
|
|
6250
6568
|
});
|
|
6569
|
+
var adminShopStoreStatusInput = object({
|
|
6570
|
+
status: _enum(["active", "archived"])
|
|
6571
|
+
});
|
|
6572
|
+
var adminShopApplicationRejectInput = object({
|
|
6573
|
+
note: string2().trim().min(1).max(500)
|
|
6574
|
+
});
|
|
6251
6575
|
var subscriptionChangeInput = object({
|
|
6252
6576
|
tierId: string2().trim().min(1).max(50)
|
|
6253
6577
|
});
|
|
@@ -6410,7 +6734,7 @@ var collabRecordListQuery = object({
|
|
|
6410
6734
|
});
|
|
6411
6735
|
var agentPermissionModes = ["confirm", "auto_edit", "yolo"];
|
|
6412
6736
|
var agentSessionKinds = ["chat", "task"];
|
|
6413
|
-
var agentMemoryTypes = ["preference", "contact", "project"];
|
|
6737
|
+
var agentMemoryTypes = ["preference", "contact", "project", "daily"];
|
|
6414
6738
|
var AGENT_TITLE_MAX = 120;
|
|
6415
6739
|
var AGENT_MEMORY_MAX = 2000;
|
|
6416
6740
|
var AGENT_PERSONA_MAX = 2000;
|
|
@@ -6426,10 +6750,17 @@ var agentToolsConfigSchema = object({
|
|
|
6426
6750
|
contacts: boolean2().optional()
|
|
6427
6751
|
}).optional()
|
|
6428
6752
|
});
|
|
6753
|
+
var agentDelegationModules = ["mail", "storage", "collab", "workspace", "web"];
|
|
6754
|
+
var agentDelegationRefSchema = object({
|
|
6755
|
+
module: _enum(agentDelegationModules),
|
|
6756
|
+
id: string2().min(1).max(200),
|
|
6757
|
+
title: string2().max(300).optional()
|
|
6758
|
+
});
|
|
6429
6759
|
var agentConfigUpdateInput = object({
|
|
6430
6760
|
activeModelId: string2().trim().min(1).max(64).optional(),
|
|
6431
6761
|
persona: string2().max(AGENT_PERSONA_MAX).optional(),
|
|
6432
6762
|
mode: _enum(agentPermissionModes).optional(),
|
|
6763
|
+
planEnabled: boolean2().optional(),
|
|
6433
6764
|
tools: agentToolsConfigSchema.optional()
|
|
6434
6765
|
});
|
|
6435
6766
|
var AGENT_PROVIDER_LABEL_MAX = 60;
|
|
@@ -6446,6 +6777,11 @@ var agentProviderModelInput = object({
|
|
|
6446
6777
|
inputImage: boolean2().optional(),
|
|
6447
6778
|
inputVideo: boolean2().optional()
|
|
6448
6779
|
});
|
|
6780
|
+
var agentSkillCreateInput = object({
|
|
6781
|
+
name: string2().trim().max(64).optional(),
|
|
6782
|
+
description: string2().trim().max(600).optional(),
|
|
6783
|
+
body: string2().min(1).max(1e5)
|
|
6784
|
+
});
|
|
6449
6785
|
var agentSessionCreateInput = object({
|
|
6450
6786
|
kind: _enum(agentSessionKinds).default("chat"),
|
|
6451
6787
|
title: string2().trim().max(AGENT_TITLE_MAX).optional(),
|
|
@@ -6454,10 +6790,35 @@ var agentSessionCreateInput = object({
|
|
|
6454
6790
|
}).refine((v) => v.kind === "chat" || !!v.title && v.title.length > 0, {
|
|
6455
6791
|
message: "task requires title"
|
|
6456
6792
|
});
|
|
6457
|
-
var agentSessionRenameInput = object({
|
|
6793
|
+
var agentSessionRenameInput = object({
|
|
6794
|
+
title: string2().trim().min(1).max(AGENT_TITLE_MAX).optional(),
|
|
6795
|
+
mode: _enum(agentPermissionModes).optional()
|
|
6796
|
+
}).refine((v) => v.title !== undefined || v.mode !== undefined, { message: "nothing to update" });
|
|
6797
|
+
var agentUsageOverviewSchema = object({
|
|
6798
|
+
monthActions: number2().int().min(0),
|
|
6799
|
+
turns30d: number2().int().min(0),
|
|
6800
|
+
daily: array(object({ day: string2().regex(/^\d{4}-\d{2}-\d{2}$/), count: number2().int().min(0) })).max(120),
|
|
6801
|
+
toolCalls: array(object({ action: string2().min(1), count: number2().int().min(0) })).max(40),
|
|
6802
|
+
approvals: object({
|
|
6803
|
+
approved: number2().int().min(0),
|
|
6804
|
+
rejected: number2().int().min(0),
|
|
6805
|
+
pending: number2().int().min(0),
|
|
6806
|
+
expired: number2().int().min(0)
|
|
6807
|
+
}),
|
|
6808
|
+
taskRuns: object({ total: number2().int().min(0), failed: number2().int().min(0) })
|
|
6809
|
+
});
|
|
6458
6810
|
var agentSessionListQuery = object({
|
|
6459
6811
|
kind: _enum(agentSessionKinds).optional(),
|
|
6460
|
-
limit: exports_coerce.number().int().min(1).max(200).optional()
|
|
6812
|
+
limit: exports_coerce.number().int().min(1).max(200).optional(),
|
|
6813
|
+
archived: preprocess((v) => v === "1" || v === "true", boolean2()).optional(),
|
|
6814
|
+
q: string2().trim().min(1).max(100).optional()
|
|
6815
|
+
});
|
|
6816
|
+
var agentTaskCreateInput = object({
|
|
6817
|
+
title: string2().trim().min(1).max(AGENT_TITLE_MAX),
|
|
6818
|
+
repeatCron: string2().trim().max(100).optional(),
|
|
6819
|
+
notifyMail: boolean2().optional(),
|
|
6820
|
+
mode: _enum(["task", "branch"]).optional(),
|
|
6821
|
+
seedMessageIds: array(string2().min(1).max(80)).max(20).optional()
|
|
6461
6822
|
});
|
|
6462
6823
|
var agentMemoryCreateInput = object({
|
|
6463
6824
|
type: _enum(agentMemoryTypes),
|
|
@@ -6815,6 +7176,9 @@ function exitCodeForApiError(body) {
|
|
|
6815
7176
|
case "collab_space_not_bound":
|
|
6816
7177
|
case "collab_space_mismatch":
|
|
6817
7178
|
return EXIT_SCOPE;
|
|
7179
|
+
case "hr_org_not_bound":
|
|
7180
|
+
case "hr_org_mismatch":
|
|
7181
|
+
return EXIT_SCOPE;
|
|
6818
7182
|
default:
|
|
6819
7183
|
return 1;
|
|
6820
7184
|
}
|
|
@@ -6955,6 +7319,67 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6955
7319
|
collab activity [--json] 协作域活动事件(Agent 推送/读取、任务创建、摘要更新)
|
|
6956
7320
|
post <taskId> (--body "..." | --body-file <path>) [--attach <fileId,...>] [--link <url,...>] [--json]
|
|
6957
7321
|
推送任务记录(糖命令:等价 collab task records 的写入)
|
|
7322
|
+
hr orgs [--json] 我的组织列表(hr 命令需令牌绑定组织:login --scopes hr --org org:<名称>)
|
|
7323
|
+
hr org create <名称> [--json] 创建公司 / 组织(创建者 owner)
|
|
7324
|
+
hr members [--org <arg>] [--json] 成员列表(角色与权限 flags)
|
|
7325
|
+
hr member add <用户名> [--title 头衔] [--org <arg>]
|
|
7326
|
+
邀请平台账号入组织(member,只读)
|
|
7327
|
+
hr member set <memberId> [--role admin|member] [--permissions org.manage=1,...] [--title]
|
|
7328
|
+
角色与权限逐项配置(owner 恒全权不可改)
|
|
7329
|
+
hr member rm <memberId> 移除成员
|
|
7330
|
+
hr invite [--reset] [--org <arg>] 常驻邀请链接(未注册者注册即入组织;--reset 作废旧链)
|
|
7331
|
+
hr people [--q 关键字] [--status active|probation|resigned] [--dept] [--limit N] [--offset N] [--json]
|
|
7332
|
+
花名册(身份证/薪资对令牌永不外泄,输出如实按无权限转述)
|
|
7333
|
+
hr person <id> [--json] 人员详情
|
|
7334
|
+
hr person create --name "姓名" --hired-at YYYY-MM-DD [--dept] [--role] [--probation-end] [--contract-type] [--contract-end] [--mobile] [--email] [--notes] [--json]
|
|
7335
|
+
建档(自动落入职事件)
|
|
7336
|
+
hr person edit <id> [--dept] [--role] [--status ...] [--contract-end YYYY-MM-DD] [--offboarded-at YYYY-MM-DD] [--json]
|
|
7337
|
+
编辑(转正/离职状态流转自动落痕)
|
|
7338
|
+
hr person offboard <id> [--date YYYY-MM-DD] [--json]
|
|
7339
|
+
办理离职(档案封存,记录依法保留)
|
|
7340
|
+
hr event add <personId> --type <onboard|contract|change|attendance|payroll|award|asset|offboard|note> --note "..." [--at YYYY-MM-DD] [--json]
|
|
7341
|
+
事件落痕(时间线即证据链)
|
|
7342
|
+
hr events [--person <id>] [--type] [--limit N] [--offset N] [--json]
|
|
7343
|
+
事件时间线
|
|
7344
|
+
hr import <文件.csv|.tsv|.json> [--org <arg>] [--json]
|
|
7345
|
+
批量建档(≤200 行;CSV/TSV 表头自动识别,姓名/入职列必填)
|
|
7346
|
+
hr tasks [--status alert|confirm|queued|done] [--kind] [--limit N] [--offset N] [--json]
|
|
7347
|
+
任务列表
|
|
7348
|
+
hr task create --kind <attendance|payroll|contract|...> --title "..." [--node] [--detail] [--due YYYY-MM-DD] [--json]
|
|
7349
|
+
新建任务
|
|
7350
|
+
hr task update <id> [--status ...] [--node] [--detail] [--due YYYY-MM-DD] [--json]
|
|
7351
|
+
任务流转
|
|
7352
|
+
hr desk [--org <arg>] [--json] 工作台聚合(Agent 首入口:任务三段 + 大事提醒 + 最近动态)
|
|
7353
|
+
shop status [--json] 店铺列表 + 开通申请进度(多店全列,★ = 当前店)
|
|
7354
|
+
shop use <slug> 切换当前管理的店铺(持久保存;店级命令缺省作用于它,
|
|
7355
|
+
--store 可单条临时覆盖;不带参数显示当前店)
|
|
7356
|
+
shop apply <店铺名> [--tagline 一句话介绍] [--json]
|
|
7357
|
+
提交开店申请(成为卖家/增开统一入口;平台审核通过自动建店,
|
|
7358
|
+
增开通过时扣积分)
|
|
7359
|
+
shop roles --store <slug> [--json] 店铺角色预设(权限组合模板 + 匹配成员数)
|
|
7360
|
+
shop members --store <slug> [--json]
|
|
7361
|
+
成员列表(身份/权限组合/业务身份/归属客户/状态)
|
|
7362
|
+
shop member edit <用户名> --perms <a,b,c> [--biz sales,support] --store <slug> [--json]
|
|
7363
|
+
编辑成员权限组合与业务身份(--perms 七项逗号分隔,
|
|
7364
|
+
--biz 省略则不改;身份=归因资格,可多选)
|
|
7365
|
+
shop invite [--reset --perms <a,b> --biz <sales,support>] --store <slug> [--json]
|
|
7366
|
+
查看 / 重置常驻邀请链接(重置后旧链接作废,
|
|
7367
|
+
受邀者权限取新默认组合;接受走链接页)
|
|
7368
|
+
shop categories [--store <slug>] [--json]
|
|
7369
|
+
分类树(多级缩进 + 商品数 + id)
|
|
7370
|
+
shop category add <名称> [--parent <id或名称>] --store <slug> [--json]
|
|
7371
|
+
新建分类(缺省顶级;新建落同级末尾)
|
|
7372
|
+
shop category rename <id或名称> <新名称> --store <slug> [--json]
|
|
7373
|
+
shop category move <id或名称> <up|down> --store <slug> [--json]
|
|
7374
|
+
同级内上移/下移一位(边界静默不动)
|
|
7375
|
+
shop category delete <id或名称> --store <slug> [--json]
|
|
7376
|
+
删除分类(有子级拒绝;叶子删除后商品迁入未分类)
|
|
7377
|
+
shop specs [--store <slug>] [--json]
|
|
7378
|
+
规格库模板(名称/选项值/引用商品数)
|
|
7379
|
+
shop spec add <名称> --values <值1,值2,…> --store <slug> [--json]
|
|
7380
|
+
shop spec update <id或名称> [--name <新名>] [--values <a,b,c>] --store <slug> [--json]
|
|
7381
|
+
shop spec delete <id或名称> --store <slug> [--json]
|
|
7382
|
+
规格模板管理(引用 = 快照拷贝,改/删不影响已建商品)
|
|
6958
7383
|
credits [--json] 积分余额(月送/充值/合计;需 credits.read scope)
|
|
6959
7384
|
credits history [--limit N] [--offset N] [--json]
|
|
6960
7385
|
积分流水(变动 ±:消耗为负;需 credits.read)
|
|
@@ -6998,10 +7423,21 @@ async function login() {
|
|
|
6998
7423
|
console.error("示例:crab login --scopes collab --space default");
|
|
6999
7424
|
return EXIT_USAGE;
|
|
7000
7425
|
}
|
|
7426
|
+
const orgArg = readFlag("--org");
|
|
7427
|
+
if (requested.some((s) => s === "hr.read" || s === "hr.write") && !orgArg) {
|
|
7428
|
+
console.error("含人事行政 scope(hr)时必须指定 --org <org:<名称>|组织id>>——令牌将绑定该组织,越界访问一律拒绝。");
|
|
7429
|
+
console.error("示例:crab login --scopes hr --org org:远帆文化");
|
|
7430
|
+
return EXIT_USAGE;
|
|
7431
|
+
}
|
|
7001
7432
|
const openBrowser = !hasFlag("--no-open");
|
|
7002
7433
|
const start = await api2(apiBase, "/v1/auth/device/start", {
|
|
7003
7434
|
method: "POST",
|
|
7004
|
-
body: JSON.stringify({
|
|
7435
|
+
body: JSON.stringify({
|
|
7436
|
+
name: `${hostname()} · crab cli`,
|
|
7437
|
+
scopes: requested,
|
|
7438
|
+
...spaceArg ? { space: spaceArg } : {},
|
|
7439
|
+
...orgArg ? { org: orgArg } : {}
|
|
7440
|
+
})
|
|
7005
7441
|
});
|
|
7006
7442
|
if (start.status !== 200) {
|
|
7007
7443
|
console.error(`发起授权失败(${start.status}):`, errorMessage(start.body, JSON.stringify(start.body)));
|
|
@@ -7045,6 +7481,8 @@ async function login() {
|
|
|
7045
7481
|
console.log(` scopes: ${poll.body.data.scopes.join(", ")}`);
|
|
7046
7482
|
if (spaceArg)
|
|
7047
7483
|
console.log(` 协作空间绑定: ${spaceArg}(collab 命令只能作用于该空间)`);
|
|
7484
|
+
if (orgArg)
|
|
7485
|
+
console.log(` HR 组织绑定: ${orgArg}(hr 命令只能作用于该组织)`);
|
|
7048
7486
|
return 0;
|
|
7049
7487
|
}
|
|
7050
7488
|
}
|
|
@@ -7187,11 +7625,13 @@ async function tokensList() {
|
|
|
7187
7625
|
return 0;
|
|
7188
7626
|
}
|
|
7189
7627
|
if (rows.length === 0) {
|
|
7190
|
-
console.log("
|
|
7628
|
+
console.log("还没有已授权的令牌。");
|
|
7191
7629
|
return 0;
|
|
7192
7630
|
}
|
|
7193
7631
|
for (const token of rows) {
|
|
7194
|
-
|
|
7632
|
+
const isApiKey = token.kind === "api";
|
|
7633
|
+
const hint = isApiKey && token.keyHint ? ` ⟨${token.keyHint}⟩` : "";
|
|
7634
|
+
console.log(`${token.id} ${isApiKey ? "[API Key] " : ""}${token.name}${hint} [${token.scopes.join(", ")}] ${token.revoked ? "已撤销" : "生效中"}${token.collabSpace ? ` ⟨空间 ${token.collabSpace}⟩` : ""}`);
|
|
7195
7635
|
console.log(` 创建 ${formatTime(token.createdAt)} · 最近使用 ${token.lastUsedAt ? formatTime(token.lastUsedAt) : "—"}${token.expiresAt ? ` · 过期 ${formatTime(token.expiresAt)}` : ""}`);
|
|
7196
7636
|
}
|
|
7197
7637
|
return 0;
|
|
@@ -9166,193 +9606,1916 @@ async function logout() {
|
|
|
9166
9606
|
console.log("✓ 已撤销 Agent Token 并清除本地凭证。");
|
|
9167
9607
|
return 0;
|
|
9168
9608
|
}
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9609
|
+
var HR_VALUED_FLAGS = [
|
|
9610
|
+
"--org",
|
|
9611
|
+
"--title",
|
|
9612
|
+
"--role",
|
|
9613
|
+
"--permissions",
|
|
9614
|
+
"--name",
|
|
9615
|
+
"--hired-at",
|
|
9616
|
+
"--employee-no",
|
|
9617
|
+
"--dept",
|
|
9618
|
+
"--role",
|
|
9619
|
+
"--status",
|
|
9620
|
+
"--probation-end",
|
|
9621
|
+
"--contract-type",
|
|
9622
|
+
"--contract-end",
|
|
9623
|
+
"--mobile",
|
|
9624
|
+
"--email",
|
|
9625
|
+
"--notes",
|
|
9626
|
+
"--type",
|
|
9627
|
+
"--note",
|
|
9628
|
+
"--at",
|
|
9629
|
+
"--date",
|
|
9630
|
+
"--kind",
|
|
9631
|
+
"--title",
|
|
9632
|
+
"--node",
|
|
9633
|
+
"--detail",
|
|
9634
|
+
"--due",
|
|
9635
|
+
"--limit",
|
|
9636
|
+
"--offset",
|
|
9637
|
+
"--q"
|
|
9638
|
+
];
|
|
9639
|
+
var HR_PERSON_STATUSES = ["active", "probation", "resigned"];
|
|
9640
|
+
var HR_TASK_STATUSES = ["alert", "confirm", "queued", "done"];
|
|
9641
|
+
var HR_EVENT_TYPES = ["onboard", "contract", "change", "attendance", "payroll", "award", "asset", "offboard", "note"];
|
|
9642
|
+
function hrOrgToken(org) {
|
|
9643
|
+
return `org:${org.name}`;
|
|
9644
|
+
}
|
|
9645
|
+
async function resolveHrOrg(credentials, arg) {
|
|
9646
|
+
const res = await api2(credentials.apiBase, "/v1/hr/orgs", { headers: authHeaders(credentials) });
|
|
9647
|
+
if (res.status !== 200) {
|
|
9648
|
+
console.error(errorMessage(res.body, "获取组织列表失败。"));
|
|
9649
|
+
const code = res.body?.error?.code;
|
|
9650
|
+
if (code === "scope_denied") {
|
|
9651
|
+
console.error("提示:hr 命令需要 hr scope——运行 crab login --scopes hr --org org:<名称> 重新授权。");
|
|
9652
|
+
}
|
|
9653
|
+
return exitCodeForApiError(res.body);
|
|
9187
9654
|
}
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
if (sub === "list")
|
|
9193
|
-
process.exit(listSkills());
|
|
9194
|
-
if (sub === "uninstall")
|
|
9195
|
-
process.exit(uninstallSkills(args.slice(2)));
|
|
9196
|
-
console.error(`用法:crab skills install | list | uninstall [--agent claude,zcode,codex,agents]
|
|
9197
|
-
`);
|
|
9198
|
-
process.exit(EXIT_USAGE);
|
|
9655
|
+
const orgs = res.body.data.orgs;
|
|
9656
|
+
if (orgs.length === 0) {
|
|
9657
|
+
console.error("你还没有加入任何公司 / 组织(在 hr-web 创建,或 crab hr org create <名称>)。");
|
|
9658
|
+
return EXIT_NOT_FOUND;
|
|
9199
9659
|
}
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
if (
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
if (sub === "search")
|
|
9209
|
-
process.exit(await mailSearch(args.slice(2).join(" ") || undefined));
|
|
9210
|
-
if (sub === "send")
|
|
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);
|
|
9660
|
+
const raw = (arg ?? "").trim();
|
|
9661
|
+
if (!raw) {
|
|
9662
|
+
if (orgs.length === 1)
|
|
9663
|
+
return orgs[0];
|
|
9664
|
+
console.error("你所在的组织不止一个,请用 --org 指定(org:<名称> / 组织 id):");
|
|
9665
|
+
for (const org of orgs)
|
|
9666
|
+
console.error(` ${hrOrgToken(org)} (成员 ${org.memberCount} · ${org.myRole})`);
|
|
9667
|
+
return EXIT_USAGE;
|
|
9223
9668
|
}
|
|
9224
|
-
|
|
9225
|
-
const
|
|
9226
|
-
if (
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
process.exit(await contactsInvitees());
|
|
9236
|
-
console.error(sub ? `未知子命令:contacts ${sub}
|
|
9237
|
-
` : `缺少子命令:contacts list | add | rm | invite | invitees
|
|
9238
|
-
`);
|
|
9239
|
-
console.log(usage);
|
|
9240
|
-
process.exit(EXIT_USAGE);
|
|
9669
|
+
if (raw.startsWith("org:")) {
|
|
9670
|
+
const name = raw.slice("org:".length).trim();
|
|
9671
|
+
if (!name) {
|
|
9672
|
+
console.error("org: 后面要跟组织名称(crab hr orgs 查看)。");
|
|
9673
|
+
return EXIT_USAGE;
|
|
9674
|
+
}
|
|
9675
|
+
const hit = orgs.filter((o) => o.name === name && o.status === "active");
|
|
9676
|
+
if (hit.length === 1)
|
|
9677
|
+
return hit[0];
|
|
9678
|
+
console.error(hit.length === 0 ? `没有名为「${name}」的 active 组织。` : `「${name}」命中多个组织,请用组织 id 指定。`);
|
|
9679
|
+
return EXIT_NOT_FOUND;
|
|
9241
9680
|
}
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
process.exit(await storageMkCat(args.slice(2)));
|
|
9268
|
-
if (sub === "rencat")
|
|
9269
|
-
process.exit(await storageRenCat(args[2], args.slice(3)));
|
|
9270
|
-
if (sub === "rmcat")
|
|
9271
|
-
process.exit(await storageRmCat(args[2]));
|
|
9272
|
-
if (sub === "tags")
|
|
9273
|
-
process.exit(await storageTags());
|
|
9274
|
-
if (sub === "share")
|
|
9275
|
-
process.exit(await storageShareCmd(args[2]));
|
|
9276
|
-
if (sub === "shares")
|
|
9277
|
-
process.exit(await storageShares(args[2]));
|
|
9278
|
-
if (sub === "unshare")
|
|
9279
|
-
process.exit(await storageUnshare(args[2]));
|
|
9280
|
-
if (sub === "key")
|
|
9281
|
-
process.exit(await storageKeyCmd());
|
|
9282
|
-
if (sub === "activity")
|
|
9283
|
-
process.exit(await storageActivity());
|
|
9284
|
-
console.error(sub ? `未知子命令:storage ${sub}
|
|
9285
|
-
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | rencat | rmcat | tags | share | shares | unshare | key | activity
|
|
9286
|
-
`);
|
|
9287
|
-
console.log(usage);
|
|
9288
|
-
process.exit(EXIT_USAGE);
|
|
9681
|
+
const byId = orgs.find((o) => o.id === raw);
|
|
9682
|
+
if (byId)
|
|
9683
|
+
return byId;
|
|
9684
|
+
const byName = orgs.filter((o) => o.name === raw);
|
|
9685
|
+
if (byName.length === 1)
|
|
9686
|
+
return byName[0];
|
|
9687
|
+
console.error(`组织标识无效:${raw}
|
|
9688
|
+
应为 org:<名称> 或组织 id(crab hr orgs 查看)。`);
|
|
9689
|
+
return EXIT_USAGE;
|
|
9690
|
+
}
|
|
9691
|
+
function parseHrDate(text) {
|
|
9692
|
+
if (text === undefined || text.trim() === "")
|
|
9693
|
+
return;
|
|
9694
|
+
const [y, m, d] = text.trim().split("-").map(Number);
|
|
9695
|
+
if (!y || !m || !d)
|
|
9696
|
+
return null;
|
|
9697
|
+
const ms = new Date(y, m - 1, d).getTime();
|
|
9698
|
+
return Number.isFinite(ms) ? ms : null;
|
|
9699
|
+
}
|
|
9700
|
+
async function resolveStoreArg(credentials) {
|
|
9701
|
+
const arg = readFlag("--store") ?? credentials.shopStore;
|
|
9702
|
+
const res = await api2(credentials.apiBase, "/v1/shop/stores", { headers: authHeaders(credentials) });
|
|
9703
|
+
if (res.status !== 200) {
|
|
9704
|
+
console.error(errorMessage(res.body, "获取店铺列表失败。"));
|
|
9705
|
+
return exitCodeForApiError(res.body);
|
|
9289
9706
|
}
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
process.exit(await agentSessions());
|
|
9296
|
-
if (sub === "approvals")
|
|
9297
|
-
process.exit(await agentApprovals());
|
|
9298
|
-
console.error(sub ? `未知子命令:agent ${sub}
|
|
9299
|
-
` : `缺少子命令:agent status | sessions | approvals
|
|
9300
|
-
`);
|
|
9301
|
-
console.log(usage);
|
|
9302
|
-
process.exit(EXIT_USAGE);
|
|
9707
|
+
const stores = res.body.data.stores;
|
|
9708
|
+
const wanted = arg ? stores.find((x) => x.slug === arg || x.id === arg) : undefined;
|
|
9709
|
+
if (arg && !wanted) {
|
|
9710
|
+
console.error(`找不到店铺 ${arg}(crab shop status 查看可用的 slug;crab shop use <slug> 切换当前店)`);
|
|
9711
|
+
return EXIT_NOT_FOUND;
|
|
9303
9712
|
}
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9713
|
+
if (wanted)
|
|
9714
|
+
return wanted;
|
|
9715
|
+
if (stores.length === 1)
|
|
9716
|
+
return stores[0];
|
|
9717
|
+
console.error("该账号有多家店铺:先 crab shop use <slug> 设置当前店,或单条命令用 --store <slug> 临时指定");
|
|
9718
|
+
return EXIT_USAGE;
|
|
9719
|
+
}
|
|
9720
|
+
var SHOP_PERMS = ["products", "orders", "aftercare", "customers", "marketing", "finance", "team", "settings"];
|
|
9721
|
+
function shopWebBase(apiBase) {
|
|
9722
|
+
if (/127\.0\.0\.1:14321|localhost:14321/.test(apiBase))
|
|
9723
|
+
return "http://127.0.0.1:14341";
|
|
9724
|
+
try {
|
|
9725
|
+
return `https://shop.${new URL(apiBase).host.replace(/^api\./, "")}`;
|
|
9726
|
+
} catch {
|
|
9727
|
+
return apiBase;
|
|
9316
9728
|
}
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
`);
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
`
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9729
|
+
}
|
|
9730
|
+
function parseCsvPermList(raw) {
|
|
9731
|
+
if (!raw)
|
|
9732
|
+
return null;
|
|
9733
|
+
const items = raw.split(",").map((x) => x.trim()).filter(Boolean);
|
|
9734
|
+
const invalid = items.filter((x) => !SHOP_PERMS.includes(x));
|
|
9735
|
+
if (invalid.length > 0) {
|
|
9736
|
+
console.error(`未知权限项:${invalid.join(", ")}
|
|
9737
|
+
可用:${SHOP_PERMS.join(", ")}`);
|
|
9738
|
+
return null;
|
|
9739
|
+
}
|
|
9740
|
+
return items;
|
|
9741
|
+
}
|
|
9742
|
+
async function shopRoles() {
|
|
9743
|
+
const credentials = requireCredentials();
|
|
9744
|
+
if (typeof credentials === "number")
|
|
9745
|
+
return credentials;
|
|
9746
|
+
const store = await resolveStoreArg(credentials);
|
|
9747
|
+
if (typeof store === "number")
|
|
9748
|
+
return store;
|
|
9749
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${store.id}/roles`, { headers: authHeaders(credentials) });
|
|
9750
|
+
if (res.status !== 200) {
|
|
9751
|
+
console.error(errorMessage(res.body, "获取角色列表失败。"));
|
|
9752
|
+
return exitCodeForApiError(res.body);
|
|
9753
|
+
}
|
|
9754
|
+
const roles = res.body.data.roles;
|
|
9755
|
+
if (hasFlag("--json")) {
|
|
9756
|
+
console.log(JSON.stringify(roles, null, 2));
|
|
9757
|
+
return 0;
|
|
9758
|
+
}
|
|
9759
|
+
console.log(`「${store.name}」角色预设(模板套用:改模板不影响已套用成员):`);
|
|
9760
|
+
for (const r of roles) {
|
|
9761
|
+
console.log(` ${r.name}${r.builtin ? "(默认)" : ""} [${r.permissions.join(", ") || "无权限"}] 匹配成员 ${r.memberCount}`);
|
|
9762
|
+
}
|
|
9763
|
+
return 0;
|
|
9764
|
+
}
|
|
9765
|
+
async function fetchStoreMembers(credentials, storeId) {
|
|
9766
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${storeId}/members`, { headers: authHeaders(credentials) });
|
|
9767
|
+
if (res.status !== 200) {
|
|
9768
|
+
console.error(errorMessage(res.body, "获取成员列表失败。"));
|
|
9769
|
+
return exitCodeForApiError(res.body);
|
|
9770
|
+
}
|
|
9771
|
+
return res.body.data.members;
|
|
9772
|
+
}
|
|
9773
|
+
async function shopMembers() {
|
|
9774
|
+
const credentials = requireCredentials();
|
|
9775
|
+
if (typeof credentials === "number")
|
|
9776
|
+
return credentials;
|
|
9777
|
+
const store = await resolveStoreArg(credentials);
|
|
9778
|
+
if (typeof store === "number")
|
|
9779
|
+
return store;
|
|
9780
|
+
const members = await fetchStoreMembers(credentials, store.id);
|
|
9781
|
+
if (typeof members === "number")
|
|
9782
|
+
return members;
|
|
9783
|
+
if (hasFlag("--json")) {
|
|
9784
|
+
console.log(JSON.stringify(members, null, 2));
|
|
9785
|
+
return 0;
|
|
9786
|
+
}
|
|
9787
|
+
console.log(`「${store.name}」成员:`);
|
|
9788
|
+
for (const m of members) {
|
|
9789
|
+
const biz = m.bizRoles.length > 0 ? m.bizRoles.join("/") : "无业务身份";
|
|
9790
|
+
console.log(` ${m.displayName || m.username} (@${m.username}) [${m.role === "owner" ? "店主" : m.permissions.join(", ") || "无权限"}] 身份: ${biz} 归属客户: ${m.assignedCustomers} ${m.status}`);
|
|
9791
|
+
}
|
|
9792
|
+
return 0;
|
|
9793
|
+
}
|
|
9794
|
+
async function shopMemberEdit() {
|
|
9795
|
+
const credentials = requireCredentials();
|
|
9796
|
+
if (typeof credentials === "number")
|
|
9797
|
+
return credentials;
|
|
9798
|
+
const positional = args.slice(3).filter((x) => !x.startsWith("--") && args[args.indexOf(x) - 1] !== "--perms" && args[args.indexOf(x) - 1] !== "--biz");
|
|
9799
|
+
const username = positional[0];
|
|
9800
|
+
const permsRaw = readFlag("--perms");
|
|
9801
|
+
const bizRaw = readFlag("--biz");
|
|
9802
|
+
if (!username || !permsRaw) {
|
|
9803
|
+
console.error(`用法:crab shop member edit <用户名> --perms <a,b,c> [--biz sales,support] --store <slug>
|
|
9804
|
+
权限项:products, orders, aftercare, customers, marketing, finance, team, settings`);
|
|
9805
|
+
return EXIT_USAGE;
|
|
9806
|
+
}
|
|
9807
|
+
const permissions = parseCsvPermList(permsRaw);
|
|
9808
|
+
if (permissions === null)
|
|
9809
|
+
return EXIT_USAGE;
|
|
9810
|
+
let bizRoles;
|
|
9811
|
+
if (bizRaw !== undefined) {
|
|
9812
|
+
bizRoles = bizRaw.split(",").map((x) => x.trim()).filter(Boolean);
|
|
9813
|
+
const invalid = bizRoles.filter((x) => x !== "sales" && x !== "support");
|
|
9814
|
+
if (invalid.length > 0) {
|
|
9815
|
+
console.error(`未知业务身份:${invalid.join(", ")}(可选 sales / support,可逗号分隔多选)`);
|
|
9816
|
+
return EXIT_USAGE;
|
|
9817
|
+
}
|
|
9818
|
+
}
|
|
9819
|
+
const store = await resolveStoreArg(credentials);
|
|
9820
|
+
if (typeof store === "number")
|
|
9821
|
+
return store;
|
|
9822
|
+
const members = await fetchStoreMembers(credentials, store.id);
|
|
9823
|
+
if (typeof members === "number")
|
|
9824
|
+
return members;
|
|
9825
|
+
const member = members.find((m) => m.username === username);
|
|
9826
|
+
if (!member) {
|
|
9827
|
+
console.error(`成员不存在:@${username}(crab shop members 查看)`);
|
|
9828
|
+
return EXIT_NOT_FOUND;
|
|
9829
|
+
}
|
|
9830
|
+
if (member.role === "owner") {
|
|
9831
|
+
console.error("店主由店铺归属决定,权限不可修改。");
|
|
9832
|
+
return 1;
|
|
9833
|
+
}
|
|
9834
|
+
const res = await api2(credentials.apiBase, `/v1/shop/members/${member.id}`, {
|
|
9835
|
+
method: "PATCH",
|
|
9836
|
+
headers: authHeaders(credentials),
|
|
9837
|
+
body: JSON.stringify({ permissions, ...bizRoles !== undefined ? { bizRoles } : {} })
|
|
9838
|
+
});
|
|
9839
|
+
if (res.status !== 200) {
|
|
9840
|
+
console.error(errorMessage(res.body, "更新成员权限失败。"));
|
|
9841
|
+
return exitCodeForApiError(res.body);
|
|
9842
|
+
}
|
|
9843
|
+
const updated = res.body.data.member;
|
|
9844
|
+
if (hasFlag("--json")) {
|
|
9845
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
9846
|
+
return 0;
|
|
9847
|
+
}
|
|
9848
|
+
console.log(`✓ @${username} 权限已更新:[${permissions.join(", ")}]${bizRoles !== undefined ? ` · 业务身份:${bizRoles.length > 0 ? bizRoles.join("/") : "无"}` : `(业务身份保持:${member.bizRoles.join("/") || "无"})`}`);
|
|
9849
|
+
return 0;
|
|
9850
|
+
}
|
|
9851
|
+
async function shopInvite() {
|
|
9852
|
+
const credentials = requireCredentials();
|
|
9853
|
+
if (typeof credentials === "number")
|
|
9854
|
+
return credentials;
|
|
9855
|
+
const store = await resolveStoreArg(credentials);
|
|
9856
|
+
if (typeof store === "number")
|
|
9857
|
+
return store;
|
|
9858
|
+
const reset = hasFlag("--reset");
|
|
9859
|
+
const permsRaw = readFlag("--perms");
|
|
9860
|
+
const bizRaw = readFlag("--biz");
|
|
9861
|
+
let body = {};
|
|
9862
|
+
if (reset) {
|
|
9863
|
+
const permissions = permsRaw ? parseCsvPermList(permsRaw) : null;
|
|
9864
|
+
if (permsRaw && permissions === null)
|
|
9865
|
+
return EXIT_USAGE;
|
|
9866
|
+
body = { permissions: permissions ?? ["orders", "customers"], bizRoles: bizRaw ? bizRaw.split(",").map((x) => x.trim()).filter(Boolean) : [] };
|
|
9867
|
+
}
|
|
9868
|
+
const path = reset ? `/v1/shop/stores/${store.id}/invite/reset` : `/v1/shop/stores/${store.id}/invite`;
|
|
9869
|
+
const res = await api2(credentials.apiBase, path, {
|
|
9870
|
+
method: reset ? "POST" : "GET",
|
|
9871
|
+
headers: authHeaders(credentials),
|
|
9872
|
+
...reset ? { body: JSON.stringify(body) } : {}
|
|
9873
|
+
});
|
|
9874
|
+
if (res.status !== 200) {
|
|
9875
|
+
console.error(errorMessage(res.body, "获取邀请链接失败。"));
|
|
9876
|
+
return exitCodeForApiError(res.body);
|
|
9877
|
+
}
|
|
9878
|
+
const invite = res.body.data.invite;
|
|
9879
|
+
const url = `${shopWebBase(credentials.apiBase)}/join/${store.slug}?t=${invite.token}`;
|
|
9880
|
+
if (hasFlag("--json")) {
|
|
9881
|
+
console.log(JSON.stringify({ ...invite, joinUrl: url }, null, 2));
|
|
9882
|
+
return 0;
|
|
9883
|
+
}
|
|
9884
|
+
console.log(`${reset ? "已重置,新邀请链接(旧链接作废):" : "常驻邀请链接:"}`);
|
|
9885
|
+
console.log(` ${url}`);
|
|
9886
|
+
console.log(` 受邀默认权限:[${invite.permissions.join(", ")}] · 业务身份:${invite.bizRoles.length > 0 ? invite.bizRoles.join("/") : "无"}`);
|
|
9887
|
+
console.log(" 对方打开链接登录后一键加入;--reset --perms <a,b> --biz <sales,support> 可重置并改默认组合。");
|
|
9888
|
+
return 0;
|
|
9889
|
+
}
|
|
9890
|
+
async function shopUse(slug) {
|
|
9891
|
+
const credentials = requireCredentials();
|
|
9892
|
+
if (typeof credentials === "number")
|
|
9893
|
+
return credentials;
|
|
9894
|
+
if (!slug) {
|
|
9895
|
+
if (credentials.shopStore)
|
|
9896
|
+
console.log(`当前店铺:${credentials.shopStore}(crab shop use <slug> 切换)`);
|
|
9897
|
+
else
|
|
9898
|
+
console.log("未设置当前店。crab shop use <slug> 设置(多店账号店级命令需要)。");
|
|
9899
|
+
return 0;
|
|
9900
|
+
}
|
|
9901
|
+
const res = await api2(credentials.apiBase, "/v1/shop/stores", { headers: authHeaders(credentials) });
|
|
9902
|
+
if (res.status !== 200) {
|
|
9903
|
+
console.error(errorMessage(res.body, "获取店铺列表失败。"));
|
|
9904
|
+
return exitCodeForApiError(res.body);
|
|
9905
|
+
}
|
|
9906
|
+
const stores = res.body.data.stores;
|
|
9907
|
+
const hit = stores.find((x) => x.slug === slug || x.id === slug);
|
|
9908
|
+
if (!hit) {
|
|
9909
|
+
console.error(`找不到店铺 ${slug}(crab shop status 查看)`);
|
|
9910
|
+
return EXIT_NOT_FOUND;
|
|
9911
|
+
}
|
|
9912
|
+
saveCredentials({ ...credentials, shopStore: hit.slug });
|
|
9913
|
+
console.log(`✓ 当前店铺切换为「${hit.name}」(${hit.slug})——店级命令缺省作用于它,--store 可临时覆盖`);
|
|
9914
|
+
return 0;
|
|
9915
|
+
}
|
|
9916
|
+
async function shopStatus() {
|
|
9917
|
+
const credentials = requireCredentials();
|
|
9918
|
+
if (typeof credentials === "number")
|
|
9919
|
+
return credentials;
|
|
9920
|
+
const json = hasFlag("--json");
|
|
9921
|
+
const [storesRes, appRes] = await Promise.all([
|
|
9922
|
+
api2(credentials.apiBase, "/v1/shop/stores", { headers: authHeaders(credentials) }),
|
|
9923
|
+
api2(credentials.apiBase, "/v1/shop/applications/mine", { headers: authHeaders(credentials) })
|
|
9924
|
+
]);
|
|
9925
|
+
if (storesRes.status !== 200) {
|
|
9926
|
+
console.error(errorMessage(storesRes.body, "获取店铺列表失败。"));
|
|
9927
|
+
return exitCodeForApiError(storesRes.body);
|
|
9928
|
+
}
|
|
9929
|
+
const stores = storesRes.body.data.stores;
|
|
9930
|
+
const application = appRes.body.data.application;
|
|
9931
|
+
if (json) {
|
|
9932
|
+
console.log(JSON.stringify({ stores, application }, null, 2));
|
|
9933
|
+
return 0;
|
|
9934
|
+
}
|
|
9935
|
+
const statusText = { pending: "审核中", approved: "已通过", rejected: "已拒绝" };
|
|
9936
|
+
if (application) {
|
|
9937
|
+
const line = `申请 ${application.name}[${application.kind === "seller" ? "卖家开通" : "增开店铺"}]:${statusText[application.status] ?? application.status}`;
|
|
9938
|
+
console.log(application.status === "rejected" ? `${line} — 理由:${application.reviewNote || "未填写"}` : line);
|
|
9939
|
+
}
|
|
9940
|
+
if (stores.length === 0) {
|
|
9941
|
+
console.log(application?.status === "pending" ? "(审核通过后店铺自动出现在这里)" : "还没有店铺。运行 crab shop apply <店铺名> 提交开店申请。");
|
|
9942
|
+
return 0;
|
|
9943
|
+
}
|
|
9944
|
+
console.log(`共 ${stores.length} 家店铺${credentials.shopStore ? "(★ = 当前店)" : ""}:`);
|
|
9945
|
+
for (const store of stores) {
|
|
9946
|
+
const star = credentials.shopStore === store.slug ? "★ " : " ";
|
|
9947
|
+
console.log(`${star}${store.name} [${store.myRole}] ${store.slug}${store.status === "archived" ? "(已下架)" : ""} id: ${store.id}`);
|
|
9948
|
+
}
|
|
9949
|
+
if (stores.length > 1)
|
|
9950
|
+
console.log("(crab shop use <slug> 切换当前店;单条命令也可 --store <slug>)");
|
|
9951
|
+
return 0;
|
|
9952
|
+
}
|
|
9953
|
+
async function shopApply(name, tagline) {
|
|
9954
|
+
const credentials = requireCredentials();
|
|
9955
|
+
if (typeof credentials === "number")
|
|
9956
|
+
return credentials;
|
|
9957
|
+
const res = await api2(credentials.apiBase, "/v1/shop/applications", {
|
|
9958
|
+
method: "POST",
|
|
9959
|
+
headers: authHeaders(credentials),
|
|
9960
|
+
body: JSON.stringify({ name, ...tagline ? { tagline } : {} })
|
|
9961
|
+
});
|
|
9962
|
+
if (res.status !== 201) {
|
|
9963
|
+
console.error(errorMessage(res.body, "提交开店申请失败。"));
|
|
9964
|
+
return exitCodeForApiError(res.body);
|
|
9965
|
+
}
|
|
9966
|
+
const application = res.body.data.application;
|
|
9967
|
+
if (hasFlag("--json")) {
|
|
9968
|
+
console.log(JSON.stringify(application, null, 2));
|
|
9969
|
+
return 0;
|
|
9970
|
+
}
|
|
9971
|
+
console.log(`✓ 申请已提交(${application.id})`);
|
|
9972
|
+
console.log(` 类型:${application.kind === "seller" ? "卖家开通(首店)" : "增开店铺"} · 状态:审核中`);
|
|
9973
|
+
console.log(" 平台审核通过后店铺自动开通(增开店铺在通过时扣积分);crab shop status 查看进度。");
|
|
9974
|
+
return 0;
|
|
9975
|
+
}
|
|
9976
|
+
function shopPositionals(start, valueFlags) {
|
|
9977
|
+
const positional = [];
|
|
9978
|
+
const rest = args.slice(start);
|
|
9979
|
+
for (let i = 0;i < rest.length; i++) {
|
|
9980
|
+
if (valueFlags.includes(rest[i])) {
|
|
9981
|
+
i++;
|
|
9982
|
+
continue;
|
|
9983
|
+
}
|
|
9984
|
+
if (rest[i]?.startsWith("--"))
|
|
9985
|
+
continue;
|
|
9986
|
+
positional.push(rest[i]);
|
|
9987
|
+
}
|
|
9988
|
+
return positional;
|
|
9989
|
+
}
|
|
9990
|
+
async function fetchStoreCategories(credentials, storeId) {
|
|
9991
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${storeId}/categories`, { headers: authHeaders(credentials) });
|
|
9992
|
+
if (res.status !== 200) {
|
|
9993
|
+
console.error(errorMessage(res.body, "获取分类列表失败。"));
|
|
9994
|
+
return exitCodeForApiError(res.body);
|
|
9995
|
+
}
|
|
9996
|
+
return res.body.data.categories;
|
|
9997
|
+
}
|
|
9998
|
+
function resolveCategoryRef(list, ref) {
|
|
9999
|
+
const byId = list.find((c) => c.id === ref);
|
|
10000
|
+
if (byId)
|
|
10001
|
+
return byId;
|
|
10002
|
+
const byName = list.filter((c) => c.name === ref);
|
|
10003
|
+
if (byName.length === 1)
|
|
10004
|
+
return byName[0];
|
|
10005
|
+
if (byName.length > 1)
|
|
10006
|
+
return `名称「${ref}」有 ${byName.length} 个同名分类,请用 id 指定(crab shop categories 查看)`;
|
|
10007
|
+
return `分类不存在:${ref}(crab shop categories 查看)`;
|
|
10008
|
+
}
|
|
10009
|
+
async function shopCategories() {
|
|
10010
|
+
const credentials = requireCredentials();
|
|
10011
|
+
if (typeof credentials === "number")
|
|
10012
|
+
return credentials;
|
|
10013
|
+
const store = await resolveStoreArg(credentials);
|
|
10014
|
+
if (typeof store === "number")
|
|
10015
|
+
return store;
|
|
10016
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10017
|
+
if (typeof categories === "number")
|
|
10018
|
+
return categories;
|
|
10019
|
+
if (hasFlag("--json")) {
|
|
10020
|
+
console.log(JSON.stringify(categories, null, 2));
|
|
10021
|
+
return 0;
|
|
10022
|
+
}
|
|
10023
|
+
if (categories.length === 0) {
|
|
10024
|
+
console.log(`「${store.name}」还没有分类。crab shop category add <名称> 创建。`);
|
|
10025
|
+
return 0;
|
|
10026
|
+
}
|
|
10027
|
+
console.log(`「${store.name}」分类树(名称后 = 商品数):`);
|
|
10028
|
+
const byParent = new Map;
|
|
10029
|
+
for (const c of categories) {
|
|
10030
|
+
const key = c.parentId ?? "";
|
|
10031
|
+
byParent.set(key, [...byParent.get(key) ?? [], c]);
|
|
10032
|
+
}
|
|
10033
|
+
const print = (parentId, depth) => {
|
|
10034
|
+
for (const c of byParent.get(parentId ?? "") ?? []) {
|
|
10035
|
+
console.log(`${" ".repeat(depth * 2)} ${c.name} (${c.productCount}) ${c.id}`);
|
|
10036
|
+
print(c.id, depth + 1);
|
|
10037
|
+
}
|
|
10038
|
+
};
|
|
10039
|
+
print(null, 0);
|
|
10040
|
+
return 0;
|
|
10041
|
+
}
|
|
10042
|
+
async function shopCategoryAdd() {
|
|
10043
|
+
const credentials = requireCredentials();
|
|
10044
|
+
if (typeof credentials === "number")
|
|
10045
|
+
return credentials;
|
|
10046
|
+
const positional = shopPositionals(3, ["--parent"]);
|
|
10047
|
+
const name = positional.join(" ").trim();
|
|
10048
|
+
const parentRef = readFlag("--parent");
|
|
10049
|
+
if (!name) {
|
|
10050
|
+
console.error("用法:crab shop category add <名称> [--parent <分类 id 或名称>] --store <slug>");
|
|
10051
|
+
return EXIT_USAGE;
|
|
10052
|
+
}
|
|
10053
|
+
const store = await resolveStoreArg(credentials);
|
|
10054
|
+
if (typeof store === "number")
|
|
10055
|
+
return store;
|
|
10056
|
+
let parentId;
|
|
10057
|
+
if (parentRef) {
|
|
10058
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10059
|
+
if (typeof categories === "number")
|
|
10060
|
+
return categories;
|
|
10061
|
+
const parent = resolveCategoryRef(categories, parentRef);
|
|
10062
|
+
if (typeof parent === "string") {
|
|
10063
|
+
console.error(parent);
|
|
10064
|
+
return EXIT_NOT_FOUND;
|
|
10065
|
+
}
|
|
10066
|
+
parentId = parent.id;
|
|
10067
|
+
}
|
|
10068
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${store.id}/categories`, {
|
|
10069
|
+
method: "POST",
|
|
10070
|
+
headers: authHeaders(credentials),
|
|
10071
|
+
body: JSON.stringify({ name, ...parentId ? { parentId } : {} })
|
|
10072
|
+
});
|
|
10073
|
+
if (res.status !== 201) {
|
|
10074
|
+
console.error(errorMessage(res.body, "创建分类失败。"));
|
|
10075
|
+
return exitCodeForApiError(res.body);
|
|
10076
|
+
}
|
|
10077
|
+
const category = res.body.data.category;
|
|
10078
|
+
if (hasFlag("--json")) {
|
|
10079
|
+
console.log(JSON.stringify(category, null, 2));
|
|
10080
|
+
return 0;
|
|
10081
|
+
}
|
|
10082
|
+
console.log(`✓ 分类已创建:${category.name}(${category.id},落同级末尾)`);
|
|
10083
|
+
return 0;
|
|
10084
|
+
}
|
|
10085
|
+
async function shopCategoryRename() {
|
|
10086
|
+
const credentials = requireCredentials();
|
|
10087
|
+
if (typeof credentials === "number")
|
|
10088
|
+
return credentials;
|
|
10089
|
+
const positional = shopPositionals(3, []);
|
|
10090
|
+
const ref = positional[0];
|
|
10091
|
+
const name = positional.slice(1).join(" ").trim();
|
|
10092
|
+
if (!ref || !name) {
|
|
10093
|
+
console.error("用法:crab shop category rename <分类 id 或名称> <新名称> --store <slug>");
|
|
10094
|
+
return EXIT_USAGE;
|
|
10095
|
+
}
|
|
10096
|
+
const store = await resolveStoreArg(credentials);
|
|
10097
|
+
if (typeof store === "number")
|
|
10098
|
+
return store;
|
|
10099
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10100
|
+
if (typeof categories === "number")
|
|
10101
|
+
return categories;
|
|
10102
|
+
const target = resolveCategoryRef(categories, ref);
|
|
10103
|
+
if (typeof target === "string") {
|
|
10104
|
+
console.error(target);
|
|
10105
|
+
return EXIT_NOT_FOUND;
|
|
10106
|
+
}
|
|
10107
|
+
const res = await api2(credentials.apiBase, `/v1/shop/categories/${target.id}`, {
|
|
10108
|
+
method: "PATCH",
|
|
10109
|
+
headers: authHeaders(credentials),
|
|
10110
|
+
body: JSON.stringify({ name })
|
|
10111
|
+
});
|
|
10112
|
+
if (res.status !== 200) {
|
|
10113
|
+
console.error(errorMessage(res.body, "重命名分类失败。"));
|
|
10114
|
+
return exitCodeForApiError(res.body);
|
|
10115
|
+
}
|
|
10116
|
+
if (hasFlag("--json"))
|
|
10117
|
+
console.log(JSON.stringify(res.body.data.category, null, 2));
|
|
10118
|
+
else
|
|
10119
|
+
console.log(`✓ 已重命名:${target.name} → ${name}`);
|
|
10120
|
+
return 0;
|
|
10121
|
+
}
|
|
10122
|
+
async function shopCategoryMove() {
|
|
10123
|
+
const credentials = requireCredentials();
|
|
10124
|
+
if (typeof credentials === "number")
|
|
10125
|
+
return credentials;
|
|
10126
|
+
const positional = shopPositionals(3, []);
|
|
10127
|
+
const ref = positional[0];
|
|
10128
|
+
const direction = positional[1];
|
|
10129
|
+
if (!ref || direction !== "up" && direction !== "down") {
|
|
10130
|
+
console.error(`用法:crab shop category move <分类 id 或名称> <up|down> --store <slug>
|
|
10131
|
+
(同级内上移/下移一位;已在边界时静默不动)`);
|
|
10132
|
+
return EXIT_USAGE;
|
|
10133
|
+
}
|
|
10134
|
+
const store = await resolveStoreArg(credentials);
|
|
10135
|
+
if (typeof store === "number")
|
|
10136
|
+
return store;
|
|
10137
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10138
|
+
if (typeof categories === "number")
|
|
10139
|
+
return categories;
|
|
10140
|
+
const target = resolveCategoryRef(categories, ref);
|
|
10141
|
+
if (typeof target === "string") {
|
|
10142
|
+
console.error(target);
|
|
10143
|
+
return EXIT_NOT_FOUND;
|
|
10144
|
+
}
|
|
10145
|
+
const res = await api2(credentials.apiBase, `/v1/shop/categories/${target.id}/move`, {
|
|
10146
|
+
method: "POST",
|
|
10147
|
+
headers: authHeaders(credentials),
|
|
10148
|
+
body: JSON.stringify({ direction })
|
|
10149
|
+
});
|
|
10150
|
+
if (res.status !== 200) {
|
|
10151
|
+
console.error(errorMessage(res.body, "移动分类失败。"));
|
|
10152
|
+
return exitCodeForApiError(res.body);
|
|
10153
|
+
}
|
|
10154
|
+
if (hasFlag("--json"))
|
|
10155
|
+
console.log(JSON.stringify(res.body.data.category, null, 2));
|
|
10156
|
+
else
|
|
10157
|
+
console.log(`✓ 已${direction === "up" ? "上移" : "下移"}:${target.name}`);
|
|
10158
|
+
return 0;
|
|
10159
|
+
}
|
|
10160
|
+
async function shopCategoryDelete() {
|
|
10161
|
+
const credentials = requireCredentials();
|
|
10162
|
+
if (typeof credentials === "number")
|
|
10163
|
+
return credentials;
|
|
10164
|
+
const positional = shopPositionals(3, []);
|
|
10165
|
+
const ref = positional[0];
|
|
10166
|
+
if (!ref) {
|
|
10167
|
+
console.error(`用法:crab shop category delete <分类 id> --store <slug>
|
|
10168
|
+
(有子分类时会被拒绝;叶子分类删除后旗下商品迁入「未分类」)`);
|
|
10169
|
+
return EXIT_USAGE;
|
|
10170
|
+
}
|
|
10171
|
+
const store = await resolveStoreArg(credentials);
|
|
10172
|
+
if (typeof store === "number")
|
|
10173
|
+
return store;
|
|
10174
|
+
const categories = await fetchStoreCategories(credentials, store.id);
|
|
10175
|
+
if (typeof categories === "number")
|
|
10176
|
+
return categories;
|
|
10177
|
+
const target = resolveCategoryRef(categories, ref);
|
|
10178
|
+
if (typeof target === "string") {
|
|
10179
|
+
console.error(target);
|
|
10180
|
+
return EXIT_NOT_FOUND;
|
|
10181
|
+
}
|
|
10182
|
+
const res = await api2(credentials.apiBase, `/v1/shop/categories/${target.id}`, {
|
|
10183
|
+
method: "DELETE",
|
|
10184
|
+
headers: authHeaders(credentials)
|
|
10185
|
+
});
|
|
10186
|
+
if (res.status !== 200) {
|
|
10187
|
+
console.error(errorMessage(res.body, "删除分类失败。"));
|
|
10188
|
+
return exitCodeForApiError(res.body);
|
|
10189
|
+
}
|
|
10190
|
+
if (hasFlag("--json"))
|
|
10191
|
+
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
10192
|
+
else
|
|
10193
|
+
console.log(`✓ 已删除分类:${target.name}(旗下商品迁入「未分类」)`);
|
|
10194
|
+
return 0;
|
|
10195
|
+
}
|
|
10196
|
+
async function fetchSpecGroups(credentials, storeId) {
|
|
10197
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${storeId}/spec-groups`, { headers: authHeaders(credentials) });
|
|
10198
|
+
if (res.status !== 200) {
|
|
10199
|
+
console.error(errorMessage(res.body, "获取规格库失败。"));
|
|
10200
|
+
return exitCodeForApiError(res.body);
|
|
10201
|
+
}
|
|
10202
|
+
return res.body.data.groups;
|
|
10203
|
+
}
|
|
10204
|
+
function resolveSpecGroupRef(list, ref) {
|
|
10205
|
+
const byId = list.find((g) => g.id === ref);
|
|
10206
|
+
if (byId)
|
|
10207
|
+
return byId;
|
|
10208
|
+
const byName = list.filter((g) => g.name === ref);
|
|
10209
|
+
if (byName.length === 1)
|
|
10210
|
+
return byName[0];
|
|
10211
|
+
if (byName.length > 1)
|
|
10212
|
+
return `名称「${ref}」有 ${byName.length} 个同名模板,请用 id 指定(crab shop specs 查看)`;
|
|
10213
|
+
return `规格模板不存在:${ref}(crab shop specs 查看)`;
|
|
10214
|
+
}
|
|
10215
|
+
function parseSpecValues(flagName) {
|
|
10216
|
+
const raw = readFlag(flagName);
|
|
10217
|
+
if (raw === undefined)
|
|
10218
|
+
return null;
|
|
10219
|
+
const values = raw.split(/[,,、]+/).map((x) => x.trim()).filter(Boolean);
|
|
10220
|
+
if (values.length === 0 || values.length > 20) {
|
|
10221
|
+
console.error("选项值需 1-20 个(逗号分隔;重复值自动去重)。");
|
|
10222
|
+
return null;
|
|
10223
|
+
}
|
|
10224
|
+
return values;
|
|
10225
|
+
}
|
|
10226
|
+
async function shopSpecs() {
|
|
10227
|
+
const credentials = requireCredentials();
|
|
10228
|
+
if (typeof credentials === "number")
|
|
10229
|
+
return credentials;
|
|
10230
|
+
const store = await resolveStoreArg(credentials);
|
|
10231
|
+
if (typeof store === "number")
|
|
10232
|
+
return store;
|
|
10233
|
+
const groups = await fetchSpecGroups(credentials, store.id);
|
|
10234
|
+
if (typeof groups === "number")
|
|
10235
|
+
return groups;
|
|
10236
|
+
if (hasFlag("--json")) {
|
|
10237
|
+
console.log(JSON.stringify(groups, null, 2));
|
|
10238
|
+
return 0;
|
|
10239
|
+
}
|
|
10240
|
+
if (groups.length === 0) {
|
|
10241
|
+
console.log(`「${store.name}」规格库还是空的。crab shop spec add <名称> --values <a,b,c> 创建模板。`);
|
|
10242
|
+
return 0;
|
|
10243
|
+
}
|
|
10244
|
+
console.log(`「${store.name}」规格库(引用 = 快照拷贝进商品,改模板不影响已建商品):`);
|
|
10245
|
+
for (const g of groups) {
|
|
10246
|
+
console.log(` ${g.name} [${g.values.join(" / ")}] 引用商品 ${g.productCount} ${g.id}`);
|
|
10247
|
+
}
|
|
10248
|
+
return 0;
|
|
10249
|
+
}
|
|
10250
|
+
async function shopSpecAdd() {
|
|
10251
|
+
const credentials = requireCredentials();
|
|
10252
|
+
if (typeof credentials === "number")
|
|
10253
|
+
return credentials;
|
|
10254
|
+
const positional = shopPositionals(3, ["--values"]);
|
|
10255
|
+
const name = positional.join(" ").trim();
|
|
10256
|
+
const values = parseSpecValues("--values");
|
|
10257
|
+
if (!name || values === null) {
|
|
10258
|
+
console.error("用法:crab shop spec add <名称> --values <值1,值2,…> --store <slug>");
|
|
10259
|
+
return EXIT_USAGE;
|
|
10260
|
+
}
|
|
10261
|
+
const store = await resolveStoreArg(credentials);
|
|
10262
|
+
if (typeof store === "number")
|
|
10263
|
+
return store;
|
|
10264
|
+
const res = await api2(credentials.apiBase, `/v1/shop/stores/${store.id}/spec-groups`, {
|
|
10265
|
+
method: "POST",
|
|
10266
|
+
headers: authHeaders(credentials),
|
|
10267
|
+
body: JSON.stringify({ name, values })
|
|
10268
|
+
});
|
|
10269
|
+
if (res.status !== 201) {
|
|
10270
|
+
console.error(errorMessage(res.body, "创建规格模板失败。"));
|
|
10271
|
+
return exitCodeForApiError(res.body);
|
|
10272
|
+
}
|
|
10273
|
+
const group = res.body.data.group;
|
|
10274
|
+
if (hasFlag("--json")) {
|
|
10275
|
+
console.log(JSON.stringify(group, null, 2));
|
|
10276
|
+
return 0;
|
|
10277
|
+
}
|
|
10278
|
+
console.log(`✓ 模板已创建:${group.name} [${group.values.join(" / ")}](${group.id})`);
|
|
10279
|
+
return 0;
|
|
10280
|
+
}
|
|
10281
|
+
async function shopSpecUpdate() {
|
|
10282
|
+
const credentials = requireCredentials();
|
|
10283
|
+
if (typeof credentials === "number")
|
|
10284
|
+
return credentials;
|
|
10285
|
+
const positional = shopPositionals(3, ["--name", "--values"]);
|
|
10286
|
+
const ref = positional[0];
|
|
10287
|
+
const name = readFlag("--name");
|
|
10288
|
+
const values = readFlag("--values") !== undefined ? parseSpecValues("--values") : undefined;
|
|
10289
|
+
if (!ref || name === undefined && values === undefined) {
|
|
10290
|
+
console.error("用法:crab shop spec update <模板 id 或名称> [--name <新名>] [--values <a,b,c>] --store <slug>");
|
|
10291
|
+
return EXIT_USAGE;
|
|
10292
|
+
}
|
|
10293
|
+
if (values === null)
|
|
10294
|
+
return EXIT_USAGE;
|
|
10295
|
+
const store = await resolveStoreArg(credentials);
|
|
10296
|
+
if (typeof store === "number")
|
|
10297
|
+
return store;
|
|
10298
|
+
const groups = await fetchSpecGroups(credentials, store.id);
|
|
10299
|
+
if (typeof groups === "number")
|
|
10300
|
+
return groups;
|
|
10301
|
+
const target = resolveSpecGroupRef(groups, ref);
|
|
10302
|
+
if (typeof target === "string") {
|
|
10303
|
+
console.error(target);
|
|
10304
|
+
return EXIT_NOT_FOUND;
|
|
10305
|
+
}
|
|
10306
|
+
const res = await api2(credentials.apiBase, `/v1/shop/spec-groups/${target.id}`, {
|
|
10307
|
+
method: "PATCH",
|
|
10308
|
+
headers: authHeaders(credentials),
|
|
10309
|
+
body: JSON.stringify({ ...name !== undefined ? { name: name.trim() } : {}, ...values !== undefined ? { values } : {} })
|
|
10310
|
+
});
|
|
10311
|
+
if (res.status !== 200) {
|
|
10312
|
+
console.error(errorMessage(res.body, "更新规格模板失败。"));
|
|
10313
|
+
return exitCodeForApiError(res.body);
|
|
10314
|
+
}
|
|
10315
|
+
const group = res.body.data.group;
|
|
10316
|
+
if (hasFlag("--json"))
|
|
10317
|
+
console.log(JSON.stringify(group, null, 2));
|
|
10318
|
+
else
|
|
10319
|
+
console.log(`✓ 模板已更新:${group.name} [${group.values.join(" / ")}](已建商品不受影响)`);
|
|
10320
|
+
return 0;
|
|
10321
|
+
}
|
|
10322
|
+
async function shopSpecDelete() {
|
|
10323
|
+
const credentials = requireCredentials();
|
|
10324
|
+
if (typeof credentials === "number")
|
|
10325
|
+
return credentials;
|
|
10326
|
+
const positional = shopPositionals(3, []);
|
|
10327
|
+
const ref = positional[0];
|
|
10328
|
+
if (!ref) {
|
|
10329
|
+
console.error(`用法:crab shop spec delete <模板 id 或名称> --store <slug>
|
|
10330
|
+
(引用 = 快照拷贝:删除不影响已建商品)`);
|
|
10331
|
+
return EXIT_USAGE;
|
|
10332
|
+
}
|
|
10333
|
+
const store = await resolveStoreArg(credentials);
|
|
10334
|
+
if (typeof store === "number")
|
|
10335
|
+
return store;
|
|
10336
|
+
const groups = await fetchSpecGroups(credentials, store.id);
|
|
10337
|
+
if (typeof groups === "number")
|
|
10338
|
+
return groups;
|
|
10339
|
+
const target = resolveSpecGroupRef(groups, ref);
|
|
10340
|
+
if (typeof target === "string") {
|
|
10341
|
+
console.error(target);
|
|
10342
|
+
return EXIT_NOT_FOUND;
|
|
10343
|
+
}
|
|
10344
|
+
const res = await api2(credentials.apiBase, `/v1/shop/spec-groups/${target.id}`, {
|
|
10345
|
+
method: "DELETE",
|
|
10346
|
+
headers: authHeaders(credentials)
|
|
10347
|
+
});
|
|
10348
|
+
if (res.status !== 200) {
|
|
10349
|
+
console.error(errorMessage(res.body, "删除规格模板失败。"));
|
|
10350
|
+
return exitCodeForApiError(res.body);
|
|
10351
|
+
}
|
|
10352
|
+
if (hasFlag("--json"))
|
|
10353
|
+
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
10354
|
+
else
|
|
10355
|
+
console.log(`✓ 已删除模板:${target.name}(已引用商品不受影响)`);
|
|
10356
|
+
return 0;
|
|
10357
|
+
}
|
|
10358
|
+
async function hrOrgsList() {
|
|
10359
|
+
const credentials = requireCredentials();
|
|
10360
|
+
if (typeof credentials === "number")
|
|
10361
|
+
return credentials;
|
|
10362
|
+
const res = await api2(credentials.apiBase, "/v1/hr/orgs", { headers: authHeaders(credentials) });
|
|
10363
|
+
if (res.status !== 200) {
|
|
10364
|
+
console.error(errorMessage(res.body, "获取组织列表失败。"));
|
|
10365
|
+
return exitCodeForApiError(res.body);
|
|
10366
|
+
}
|
|
10367
|
+
const orgs = res.body.data.orgs;
|
|
10368
|
+
if (hasFlag("--json")) {
|
|
10369
|
+
console.log(JSON.stringify(orgs, null, 2));
|
|
10370
|
+
return 0;
|
|
10371
|
+
}
|
|
10372
|
+
if (orgs.length === 0) {
|
|
10373
|
+
console.log("还没有公司 / 组织。");
|
|
10374
|
+
return 0;
|
|
10375
|
+
}
|
|
10376
|
+
for (const org of orgs) {
|
|
10377
|
+
console.log(`${org.name} [${org.myRole}] ${hrOrgToken(org)} id: ${org.id}`);
|
|
10378
|
+
}
|
|
10379
|
+
return 0;
|
|
10380
|
+
}
|
|
10381
|
+
async function hrOrgCreate(name) {
|
|
10382
|
+
if (!name) {
|
|
10383
|
+
console.error("用法:crab hr org create <名称> [--json]");
|
|
10384
|
+
return EXIT_USAGE;
|
|
10385
|
+
}
|
|
10386
|
+
const credentials = requireCredentials();
|
|
10387
|
+
if (typeof credentials === "number")
|
|
10388
|
+
return credentials;
|
|
10389
|
+
const res = await api2(credentials.apiBase, "/v1/hr/orgs", {
|
|
10390
|
+
method: "POST",
|
|
10391
|
+
headers: authHeaders(credentials),
|
|
10392
|
+
body: JSON.stringify({ name })
|
|
10393
|
+
});
|
|
10394
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
10395
|
+
console.error(errorMessage(res.body, "创建组织失败。"));
|
|
10396
|
+
return exitCodeForApiError(res.body);
|
|
10397
|
+
}
|
|
10398
|
+
const org = res.body.data.org;
|
|
10399
|
+
if (hasFlag("--json")) {
|
|
10400
|
+
console.log(JSON.stringify(org, null, 2));
|
|
10401
|
+
return 0;
|
|
10402
|
+
}
|
|
10403
|
+
console.log(`✓ 已创建 ${org.name}(你是 owner)。提示:重新授权绑定该组织:crab login --scopes hr --org ${hrOrgToken(org)}`);
|
|
10404
|
+
return 0;
|
|
10405
|
+
}
|
|
10406
|
+
async function hrMembersList() {
|
|
10407
|
+
const credentials = requireCredentials();
|
|
10408
|
+
if (typeof credentials === "number")
|
|
10409
|
+
return credentials;
|
|
10410
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10411
|
+
if (typeof org === "number")
|
|
10412
|
+
return org;
|
|
10413
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/members`, { headers: authHeaders(credentials) });
|
|
10414
|
+
if (res.status !== 200) {
|
|
10415
|
+
console.error(errorMessage(res.body, "获取成员列表失败。"));
|
|
10416
|
+
return exitCodeForApiError(res.body);
|
|
10417
|
+
}
|
|
10418
|
+
const members = res.body.data.members;
|
|
10419
|
+
if (hasFlag("--json")) {
|
|
10420
|
+
console.log(JSON.stringify(members, null, 2));
|
|
10421
|
+
return 0;
|
|
10422
|
+
}
|
|
10423
|
+
console.log(`组织:${hrOrgToken(org)}`);
|
|
10424
|
+
for (const m of members) {
|
|
10425
|
+
const flags = Object.entries(m.permissions).filter(([, v]) => v).map(([k]) => k).join(",") || "只读";
|
|
10426
|
+
console.log(`${m.username}${m.title ? `(${m.title})` : ""} [${m.role}] ${flags} id: ${m.id}${m.status !== "active" ? ` · ${m.status}` : ""}`);
|
|
10427
|
+
}
|
|
10428
|
+
return 0;
|
|
10429
|
+
}
|
|
10430
|
+
async function hrMemberAdd() {
|
|
10431
|
+
const credentials = requireCredentials();
|
|
10432
|
+
if (typeof credentials === "number")
|
|
10433
|
+
return credentials;
|
|
10434
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10435
|
+
if (typeof org === "number")
|
|
10436
|
+
return org;
|
|
10437
|
+
const username = positionalArgs(args.slice(3), HR_VALUED_FLAGS);
|
|
10438
|
+
const target = username[0];
|
|
10439
|
+
if (!target) {
|
|
10440
|
+
console.error("用法:crab hr member add <用户名> [--title 头衔] [--org <arg>]");
|
|
10441
|
+
return EXIT_USAGE;
|
|
10442
|
+
}
|
|
10443
|
+
const title = readFlag("--title");
|
|
10444
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/members`, {
|
|
10445
|
+
method: "POST",
|
|
10446
|
+
headers: authHeaders(credentials),
|
|
10447
|
+
body: JSON.stringify({ username: target, ...title ? { title } : {} })
|
|
10448
|
+
});
|
|
10449
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
10450
|
+
console.error(errorMessage(res.body, "邀请成员失败。"));
|
|
10451
|
+
return exitCodeForApiError(res.body);
|
|
10452
|
+
}
|
|
10453
|
+
if (hasFlag("--json"))
|
|
10454
|
+
console.log(JSON.stringify(res.body.data, null, 2));
|
|
10455
|
+
else
|
|
10456
|
+
console.log(`✓ 已把 ${target} 加入 ${org.name}(member,只读;用 hr member set 提权)。`);
|
|
10457
|
+
return 0;
|
|
10458
|
+
}
|
|
10459
|
+
function parsePermissionFlags(text) {
|
|
10460
|
+
if (!text)
|
|
10461
|
+
return;
|
|
10462
|
+
const out = {};
|
|
10463
|
+
for (const pair of text.split(",").map((x) => x.trim()).filter(Boolean)) {
|
|
10464
|
+
const [key, value] = pair.split("=");
|
|
10465
|
+
if (!key)
|
|
10466
|
+
continue;
|
|
10467
|
+
out[key.trim()] = value !== "0" && value?.toLowerCase() !== "false";
|
|
10468
|
+
}
|
|
10469
|
+
return out;
|
|
10470
|
+
}
|
|
10471
|
+
async function hrMemberSet() {
|
|
10472
|
+
const credentials = requireCredentials();
|
|
10473
|
+
if (typeof credentials === "number")
|
|
10474
|
+
return credentials;
|
|
10475
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10476
|
+
if (typeof org === "number")
|
|
10477
|
+
return org;
|
|
10478
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
10479
|
+
const role = readFlag("--role");
|
|
10480
|
+
const permissions = parsePermissionFlags(readFlag("--permissions"));
|
|
10481
|
+
const title = readFlag("--title");
|
|
10482
|
+
if (!target || !role && !permissions && title === undefined) {
|
|
10483
|
+
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>]");
|
|
10484
|
+
return EXIT_USAGE;
|
|
10485
|
+
}
|
|
10486
|
+
if (role && role !== "admin" && role !== "member") {
|
|
10487
|
+
console.error("--role 只接受 admin | member(owner 不可改)。");
|
|
10488
|
+
return EXIT_USAGE;
|
|
10489
|
+
}
|
|
10490
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/members/${encodeURIComponent(target)}`, {
|
|
10491
|
+
method: "PATCH",
|
|
10492
|
+
headers: authHeaders(credentials),
|
|
10493
|
+
body: JSON.stringify({ ...role ? { role } : {}, ...permissions ? { permissions } : {}, ...title !== undefined ? { title } : {} })
|
|
10494
|
+
});
|
|
10495
|
+
if (res.status !== 200) {
|
|
10496
|
+
console.error(errorMessage(res.body, "更新成员失败。"));
|
|
10497
|
+
return exitCodeForApiError(res.body);
|
|
10498
|
+
}
|
|
10499
|
+
const member = res.body.data.member;
|
|
10500
|
+
if (hasFlag("--json"))
|
|
10501
|
+
console.log(JSON.stringify(member, null, 2));
|
|
10502
|
+
else
|
|
10503
|
+
console.log(`✓ 已更新 ${member?.username ?? target}(${member?.role ?? "?"})。`);
|
|
10504
|
+
return 0;
|
|
10505
|
+
}
|
|
10506
|
+
async function hrMemberRm() {
|
|
10507
|
+
const credentials = requireCredentials();
|
|
10508
|
+
if (typeof credentials === "number")
|
|
10509
|
+
return credentials;
|
|
10510
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10511
|
+
if (typeof org === "number")
|
|
10512
|
+
return org;
|
|
10513
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
10514
|
+
if (!target) {
|
|
10515
|
+
console.error("用法:crab hr member rm <memberId> [--org <arg>](id 见 crab hr members)");
|
|
10516
|
+
return EXIT_USAGE;
|
|
10517
|
+
}
|
|
10518
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/members/${encodeURIComponent(target)}`, {
|
|
10519
|
+
method: "DELETE",
|
|
10520
|
+
headers: authHeaders(credentials)
|
|
10521
|
+
});
|
|
10522
|
+
if (res.status !== 200) {
|
|
10523
|
+
console.error(errorMessage(res.body, "移除成员失败。"));
|
|
10524
|
+
return exitCodeForApiError(res.body);
|
|
10525
|
+
}
|
|
10526
|
+
if (hasFlag("--json"))
|
|
10527
|
+
console.log(JSON.stringify(res.body.data, null, 2));
|
|
10528
|
+
else
|
|
10529
|
+
console.log("✓ 已移除成员(可随时重新邀请)。");
|
|
10530
|
+
return 0;
|
|
10531
|
+
}
|
|
10532
|
+
async function hrInvite() {
|
|
10533
|
+
const credentials = requireCredentials();
|
|
10534
|
+
if (typeof credentials === "number")
|
|
10535
|
+
return credentials;
|
|
10536
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10537
|
+
if (typeof org === "number")
|
|
10538
|
+
return org;
|
|
10539
|
+
const path = hasFlag("--reset") ? "/invite/reset" : "/invite";
|
|
10540
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}${path}`, { method: hasFlag("--reset") ? "POST" : "GET", headers: authHeaders(credentials) });
|
|
10541
|
+
if (res.status !== 200) {
|
|
10542
|
+
console.error(errorMessage(res.body, "获取邀请链接失败。"));
|
|
10543
|
+
return exitCodeForApiError(res.body);
|
|
10544
|
+
}
|
|
10545
|
+
const invite = res.body.data.invite;
|
|
10546
|
+
const main = credentials.apiBase.includes("127.0.0.1") ? "http://127.0.0.1:14331" : "https://crabcloud.cc";
|
|
10547
|
+
const link = `${main}/register?invite=${invite.token}`;
|
|
10548
|
+
if (hasFlag("--json"))
|
|
10549
|
+
console.log(JSON.stringify({ ...invite, link }, null, 2));
|
|
10550
|
+
else {
|
|
10551
|
+
if (hasFlag("--reset"))
|
|
10552
|
+
console.log("✓ 已重置(旧链接立即失效)。");
|
|
10553
|
+
console.log(`邀请链接:${link}`);
|
|
10554
|
+
console.log("未注册的老板 / 同事打开此链接,注册后自动加入组织。");
|
|
10555
|
+
}
|
|
10556
|
+
return 0;
|
|
10557
|
+
}
|
|
10558
|
+
async function hrPeopleList() {
|
|
10559
|
+
const credentials = requireCredentials();
|
|
10560
|
+
if (typeof credentials === "number")
|
|
10561
|
+
return credentials;
|
|
10562
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10563
|
+
if (typeof org === "number")
|
|
10564
|
+
return org;
|
|
10565
|
+
const status = readFlag("--status");
|
|
10566
|
+
if (status && !HR_PERSON_STATUSES.includes(status)) {
|
|
10567
|
+
console.error(`未知状态:${status}
|
|
10568
|
+
可用:${HR_PERSON_STATUSES.join(", ")}`);
|
|
10569
|
+
return EXIT_USAGE;
|
|
10570
|
+
}
|
|
10571
|
+
const params = new URLSearchParams;
|
|
10572
|
+
const q = readFlag("--q");
|
|
10573
|
+
if (q)
|
|
10574
|
+
params.set("q", q);
|
|
10575
|
+
if (status)
|
|
10576
|
+
params.set("status", status);
|
|
10577
|
+
const dept = readFlag("--dept");
|
|
10578
|
+
if (dept)
|
|
10579
|
+
params.set("dept", dept);
|
|
10580
|
+
const limit = readFlag("--limit");
|
|
10581
|
+
if (limit)
|
|
10582
|
+
params.set("limit", limit);
|
|
10583
|
+
const offset = readFlag("--offset");
|
|
10584
|
+
if (offset)
|
|
10585
|
+
params.set("offset", offset);
|
|
10586
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people?${params}`, { headers: authHeaders(credentials) });
|
|
10587
|
+
if (res.status !== 200) {
|
|
10588
|
+
console.error(errorMessage(res.body, "获取花名册失败。"));
|
|
10589
|
+
return exitCodeForApiError(res.body);
|
|
10590
|
+
}
|
|
10591
|
+
const page = res.body.data;
|
|
10592
|
+
if (hasFlag("--json")) {
|
|
10593
|
+
console.log(JSON.stringify({ ...page, org: { id: org.id, name: org.name } }, null, 2));
|
|
10594
|
+
return 0;
|
|
10595
|
+
}
|
|
10596
|
+
console.log(`组织:${hrOrgToken(org)} · 共 ${page.total} 人`);
|
|
10597
|
+
if (page.people.length === 0) {
|
|
10598
|
+
console.log('没有匹配的人员(crab hr person create --name "…" --hired-at YYYY-MM-DD 建档)。');
|
|
10599
|
+
return 0;
|
|
10600
|
+
}
|
|
10601
|
+
for (const p of page.people) {
|
|
10602
|
+
const day = (ms) => ms ? new Date(ms).toISOString().slice(0, 10) : "—";
|
|
10603
|
+
console.log(`${p.name} ${p.dept ? `${p.dept} · ` : ""}${p.role || "员工"} [${p.status}] 入职 ${day(p.hiredAt)} id: ${p.id}`);
|
|
10604
|
+
if (p.alert)
|
|
10605
|
+
console.log(` ⚠ ${p.alert}`);
|
|
10606
|
+
}
|
|
10607
|
+
if (page.people.length < page.total)
|
|
10608
|
+
console.log(`(还有更多:--offset ${(Number(offset ?? 0) || 0) + page.people.length})`);
|
|
10609
|
+
return 0;
|
|
10610
|
+
}
|
|
10611
|
+
async function hrPersonDetail(id) {
|
|
10612
|
+
if (!id) {
|
|
10613
|
+
console.error("用法:crab hr person <id> [--json](id 见 crab hr people)");
|
|
10614
|
+
return EXIT_USAGE;
|
|
10615
|
+
}
|
|
10616
|
+
const credentials = requireCredentials();
|
|
10617
|
+
if (typeof credentials === "number")
|
|
10618
|
+
return credentials;
|
|
10619
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10620
|
+
if (typeof org === "number")
|
|
10621
|
+
return org;
|
|
10622
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people?q=${encodeURIComponent(id)}&limit=50`, { headers: authHeaders(credentials) });
|
|
10623
|
+
if (res.status !== 200) {
|
|
10624
|
+
console.error(errorMessage(res.body, "获取人员失败。"));
|
|
10625
|
+
return exitCodeForApiError(res.body);
|
|
10626
|
+
}
|
|
10627
|
+
const people = res.body.data.people;
|
|
10628
|
+
const person = people.find((p) => p.id === id);
|
|
10629
|
+
if (!person) {
|
|
10630
|
+
console.error("人员不存在或不属于当前组织。");
|
|
10631
|
+
return EXIT_NOT_FOUND;
|
|
10632
|
+
}
|
|
10633
|
+
if (hasFlag("--json")) {
|
|
10634
|
+
console.log(JSON.stringify(person, null, 2));
|
|
10635
|
+
return 0;
|
|
10636
|
+
}
|
|
10637
|
+
const p = person;
|
|
10638
|
+
const day = (ms) => ms ? new Date(ms).toISOString().slice(0, 10) : "—";
|
|
10639
|
+
console.log(`${p.name} ${p.dept ? `${p.dept} · ` : ""}${p.role || "员工"} [${p.status}]`);
|
|
10640
|
+
console.log(` 入职 ${day(p.hiredAt)} · 合同 ${p.contractType || "—"}(${day(p.contractEnd)})`);
|
|
10641
|
+
if (p.mobile || p.email)
|
|
10642
|
+
console.log(` 联系方式: ${p.mobile || "—"} / ${p.email || "—"}`);
|
|
10643
|
+
console.log(` 身份证: ${p.idNumber ?? "(无权限)"} · 薪资: ${p.salaryCents != null ? `¥${(p.salaryCents / 100).toFixed(2)}` : "(无权限)"}`);
|
|
10644
|
+
if (p.alert)
|
|
10645
|
+
console.log(` ⚠ ${p.alert}`);
|
|
10646
|
+
console.log(` 时间线: crab hr events --person ${p.id}`);
|
|
10647
|
+
return 0;
|
|
10648
|
+
}
|
|
10649
|
+
async function hrPersonCreate() {
|
|
10650
|
+
const name = readFlag("--name");
|
|
10651
|
+
const hiredFlag = readFlag("--hired-at");
|
|
10652
|
+
if (!name || !hiredFlag) {
|
|
10653
|
+
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]');
|
|
10654
|
+
return EXIT_USAGE;
|
|
10655
|
+
}
|
|
10656
|
+
const hired = parseHrDate(hiredFlag);
|
|
10657
|
+
if (hired == null) {
|
|
10658
|
+
console.error("--hired-at 格式应为 YYYY-MM-DD。");
|
|
10659
|
+
return EXIT_USAGE;
|
|
10660
|
+
}
|
|
10661
|
+
const credentials = requireCredentials();
|
|
10662
|
+
if (typeof credentials === "number")
|
|
10663
|
+
return credentials;
|
|
10664
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10665
|
+
if (typeof org === "number")
|
|
10666
|
+
return org;
|
|
10667
|
+
const probation = parseHrDate(readFlag("--probation-end"));
|
|
10668
|
+
if (probation === null) {
|
|
10669
|
+
console.error("--probation-end 格式应为 YYYY-MM-DD。");
|
|
10670
|
+
return EXIT_USAGE;
|
|
10671
|
+
}
|
|
10672
|
+
const contractEnd = parseHrDate(readFlag("--contract-end"));
|
|
10673
|
+
if (contractEnd === null) {
|
|
10674
|
+
console.error("--contract-end 格式应为 YYYY-MM-DD。");
|
|
10675
|
+
return EXIT_USAGE;
|
|
10676
|
+
}
|
|
10677
|
+
const status = readFlag("--status") ?? "active";
|
|
10678
|
+
if (!HR_PERSON_STATUSES.includes(status) || status === "resigned") {
|
|
10679
|
+
console.error("--status 只接受 active | probation(建档不直接建离职)。");
|
|
10680
|
+
return EXIT_USAGE;
|
|
10681
|
+
}
|
|
10682
|
+
const payload = {
|
|
10683
|
+
name,
|
|
10684
|
+
hiredAt: hired,
|
|
10685
|
+
status,
|
|
10686
|
+
employeeNo: readFlag("--employee-no") ?? "",
|
|
10687
|
+
dept: readFlag("--dept") ?? "",
|
|
10688
|
+
role: readFlag("--role") ?? "",
|
|
10689
|
+
mobile: readFlag("--mobile") ?? "",
|
|
10690
|
+
email: readFlag("--email") ?? "",
|
|
10691
|
+
notes: readFlag("--notes") ?? ""
|
|
10692
|
+
};
|
|
10693
|
+
if (probation != null)
|
|
10694
|
+
payload.probationEnd = probation;
|
|
10695
|
+
const contractType = readFlag("--contract-type");
|
|
10696
|
+
if (contractType)
|
|
10697
|
+
payload.contractType = contractType;
|
|
10698
|
+
if (contractEnd != null)
|
|
10699
|
+
payload.contractEnd = contractEnd;
|
|
10700
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people`, {
|
|
10701
|
+
method: "POST",
|
|
10702
|
+
headers: authHeaders(credentials),
|
|
10703
|
+
body: JSON.stringify(payload)
|
|
10704
|
+
});
|
|
10705
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
10706
|
+
console.error(errorMessage(res.body, "建档失败。"));
|
|
10707
|
+
return exitCodeForApiError(res.body);
|
|
10708
|
+
}
|
|
10709
|
+
const person = res.body.data.person;
|
|
10710
|
+
if (hasFlag("--json"))
|
|
10711
|
+
console.log(JSON.stringify(person, null, 2));
|
|
10712
|
+
else
|
|
10713
|
+
console.log(`✓ 已建档 ${person.name}(自动落入职事件)· id: ${person.id}`);
|
|
10714
|
+
return 0;
|
|
10715
|
+
}
|
|
10716
|
+
async function hrPersonEdit() {
|
|
10717
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
10718
|
+
if (!target) {
|
|
10719
|
+
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]");
|
|
10720
|
+
return EXIT_USAGE;
|
|
10721
|
+
}
|
|
10722
|
+
const credentials = requireCredentials();
|
|
10723
|
+
if (typeof credentials === "number")
|
|
10724
|
+
return credentials;
|
|
10725
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10726
|
+
if (typeof org === "number")
|
|
10727
|
+
return org;
|
|
10728
|
+
const payload = {};
|
|
10729
|
+
const simple = [
|
|
10730
|
+
["--dept", "dept"],
|
|
10731
|
+
["--role", "role"],
|
|
10732
|
+
["--contract-type", "contractType"],
|
|
10733
|
+
["--mobile", "mobile"],
|
|
10734
|
+
["--email", "email"],
|
|
10735
|
+
["--notes", "notes"]
|
|
10736
|
+
];
|
|
10737
|
+
for (const [flag, key] of simple) {
|
|
10738
|
+
const value = readFlag(flag);
|
|
10739
|
+
if (value !== undefined)
|
|
10740
|
+
payload[key] = value;
|
|
10741
|
+
}
|
|
10742
|
+
const status = readFlag("--status");
|
|
10743
|
+
if (status) {
|
|
10744
|
+
if (!HR_PERSON_STATUSES.includes(status)) {
|
|
10745
|
+
console.error(`--status 只接受:${HR_PERSON_STATUSES.join(", ")}`);
|
|
10746
|
+
return EXIT_USAGE;
|
|
10747
|
+
}
|
|
10748
|
+
payload.status = status;
|
|
10749
|
+
}
|
|
10750
|
+
const contractEnd = parseHrDate(readFlag("--contract-end"));
|
|
10751
|
+
if (contractEnd === null) {
|
|
10752
|
+
console.error("--contract-end 格式应为 YYYY-MM-DD。");
|
|
10753
|
+
return EXIT_USAGE;
|
|
10754
|
+
}
|
|
10755
|
+
if (contractEnd != null)
|
|
10756
|
+
payload.contractEnd = contractEnd;
|
|
10757
|
+
const offboarded = parseHrDate(readFlag("--offboarded-at"));
|
|
10758
|
+
if (offboarded === null) {
|
|
10759
|
+
console.error("--offboarded-at 格式应为 YYYY-MM-DD。");
|
|
10760
|
+
return EXIT_USAGE;
|
|
10761
|
+
}
|
|
10762
|
+
if (offboarded != null)
|
|
10763
|
+
payload.offboardedAt = offboarded;
|
|
10764
|
+
if (Object.keys(payload).length === 0) {
|
|
10765
|
+
console.error("没有要更新的字段。");
|
|
10766
|
+
return EXIT_USAGE;
|
|
10767
|
+
}
|
|
10768
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/${encodeURIComponent(target)}`, {
|
|
10769
|
+
method: "PATCH",
|
|
10770
|
+
headers: authHeaders(credentials),
|
|
10771
|
+
body: JSON.stringify(payload)
|
|
10772
|
+
});
|
|
10773
|
+
if (res.status !== 200) {
|
|
10774
|
+
console.error(errorMessage(res.body, "更新失败。"));
|
|
10775
|
+
return exitCodeForApiError(res.body);
|
|
10776
|
+
}
|
|
10777
|
+
const person = res.body.data.person;
|
|
10778
|
+
if (hasFlag("--json"))
|
|
10779
|
+
console.log(JSON.stringify(person, null, 2));
|
|
10780
|
+
else
|
|
10781
|
+
console.log(`✓ 已更新 ${person?.name ?? target}(${person?.status ?? "?"};状态流转自动落痕)。`);
|
|
10782
|
+
return 0;
|
|
10783
|
+
}
|
|
10784
|
+
async function hrPersonOffboard() {
|
|
10785
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
10786
|
+
if (!target) {
|
|
10787
|
+
console.error("用法:crab hr person offboard <id> [--date YYYY-MM-DD] [--json](离职封存不删,记录依法保留)");
|
|
10788
|
+
return EXIT_USAGE;
|
|
10789
|
+
}
|
|
10790
|
+
const credentials = requireCredentials();
|
|
10791
|
+
if (typeof credentials === "number")
|
|
10792
|
+
return credentials;
|
|
10793
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10794
|
+
if (typeof org === "number")
|
|
10795
|
+
return org;
|
|
10796
|
+
const date = parseHrDate(readFlag("--date"));
|
|
10797
|
+
if (date === null) {
|
|
10798
|
+
console.error("--date 格式应为 YYYY-MM-DD。");
|
|
10799
|
+
return EXIT_USAGE;
|
|
10800
|
+
}
|
|
10801
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/${encodeURIComponent(target)}`, {
|
|
10802
|
+
method: "PATCH",
|
|
10803
|
+
headers: authHeaders(credentials),
|
|
10804
|
+
body: JSON.stringify({ status: "resigned", ...date != null ? { offboardedAt: date } : {} })
|
|
10805
|
+
});
|
|
10806
|
+
if (res.status !== 200) {
|
|
10807
|
+
console.error(errorMessage(res.body, "离职办理失败。"));
|
|
10808
|
+
return exitCodeForApiError(res.body);
|
|
10809
|
+
}
|
|
10810
|
+
const person = res.body.data.person;
|
|
10811
|
+
if (hasFlag("--json"))
|
|
10812
|
+
console.log(JSON.stringify(person, null, 2));
|
|
10813
|
+
else
|
|
10814
|
+
console.log(`✓ ${person?.name ?? target} 已离职(档案封存,离职事件已落时间线)。`);
|
|
10815
|
+
return 0;
|
|
10816
|
+
}
|
|
10817
|
+
async function hrEventAdd() {
|
|
10818
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
10819
|
+
const type = readFlag("--type");
|
|
10820
|
+
const note = readFlag("--note");
|
|
10821
|
+
if (!target || !type || !note) {
|
|
10822
|
+
console.error(`用法:crab hr event add <personId> --type <${HR_EVENT_TYPES.join("|")}> --note "记录内容" [--at YYYY-MM-DD] [--json]`);
|
|
10823
|
+
return EXIT_USAGE;
|
|
10824
|
+
}
|
|
10825
|
+
if (!HR_EVENT_TYPES.includes(type)) {
|
|
10826
|
+
console.error(`未知事件类型:${type}
|
|
10827
|
+
可用:${HR_EVENT_TYPES.join(", ")}`);
|
|
10828
|
+
return EXIT_USAGE;
|
|
10829
|
+
}
|
|
10830
|
+
const at = parseHrDate(readFlag("--at"));
|
|
10831
|
+
if (at === null) {
|
|
10832
|
+
console.error("--at 格式应为 YYYY-MM-DD。");
|
|
10833
|
+
return EXIT_USAGE;
|
|
10834
|
+
}
|
|
10835
|
+
const credentials = requireCredentials();
|
|
10836
|
+
if (typeof credentials === "number")
|
|
10837
|
+
return credentials;
|
|
10838
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10839
|
+
if (typeof org === "number")
|
|
10840
|
+
return org;
|
|
10841
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/${encodeURIComponent(target)}/events`, {
|
|
10842
|
+
method: "POST",
|
|
10843
|
+
headers: authHeaders(credentials),
|
|
10844
|
+
body: JSON.stringify({ type, note, ...at != null ? { at } : {} })
|
|
10845
|
+
});
|
|
10846
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
10847
|
+
console.error(errorMessage(res.body, "落痕失败。"));
|
|
10848
|
+
return exitCodeForApiError(res.body);
|
|
10849
|
+
}
|
|
10850
|
+
const event = res.body.data.event;
|
|
10851
|
+
if (hasFlag("--json"))
|
|
10852
|
+
console.log(JSON.stringify(event, null, 2));
|
|
10853
|
+
else
|
|
10854
|
+
console.log(`✓ 已落痕(${type})· 事件 id: ${event.id}`);
|
|
10855
|
+
return 0;
|
|
10856
|
+
}
|
|
10857
|
+
async function hrEventsList() {
|
|
10858
|
+
const credentials = requireCredentials();
|
|
10859
|
+
if (typeof credentials === "number")
|
|
10860
|
+
return credentials;
|
|
10861
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10862
|
+
if (typeof org === "number")
|
|
10863
|
+
return org;
|
|
10864
|
+
const type = readFlag("--type");
|
|
10865
|
+
if (type && !HR_EVENT_TYPES.includes(type)) {
|
|
10866
|
+
console.error(`未知事件类型:${type}
|
|
10867
|
+
可用:${HR_EVENT_TYPES.join(", ")}`);
|
|
10868
|
+
return EXIT_USAGE;
|
|
10869
|
+
}
|
|
10870
|
+
const params = new URLSearchParams;
|
|
10871
|
+
const person = readFlag("--person");
|
|
10872
|
+
if (person)
|
|
10873
|
+
params.set("personId", person);
|
|
10874
|
+
if (type)
|
|
10875
|
+
params.set("type", type);
|
|
10876
|
+
const limit = readFlag("--limit");
|
|
10877
|
+
if (limit)
|
|
10878
|
+
params.set("limit", limit);
|
|
10879
|
+
const offset = readFlag("--offset");
|
|
10880
|
+
if (offset)
|
|
10881
|
+
params.set("offset", offset);
|
|
10882
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/events?${params}`, { headers: authHeaders(credentials) });
|
|
10883
|
+
if (res.status !== 200) {
|
|
10884
|
+
console.error(errorMessage(res.body, "获取时间线失败。"));
|
|
10885
|
+
return exitCodeForApiError(res.body);
|
|
10886
|
+
}
|
|
10887
|
+
const page = res.body.data;
|
|
10888
|
+
if (hasFlag("--json")) {
|
|
10889
|
+
console.log(JSON.stringify({ ...page, org: { id: org.id, name: org.name } }, null, 2));
|
|
10890
|
+
return 0;
|
|
10891
|
+
}
|
|
10892
|
+
console.log(`组织:${hrOrgToken(org)} · 共 ${page.total} 条`);
|
|
10893
|
+
for (const event of page.events) {
|
|
10894
|
+
const day = new Date(event.at).toISOString().slice(0, 10);
|
|
10895
|
+
console.log(`${day} [${event.type}] ${event.note} person: ${event.personId}`);
|
|
10896
|
+
}
|
|
10897
|
+
if (page.events.length < page.total)
|
|
10898
|
+
console.log(`(还有更多:--offset ${(Number(offset ?? 0) || 0) + page.events.length})`);
|
|
10899
|
+
return 0;
|
|
10900
|
+
}
|
|
10901
|
+
function parseHrImportFile(text) {
|
|
10902
|
+
try {
|
|
10903
|
+
const parsed = JSON.parse(text);
|
|
10904
|
+
if (Array.isArray(parsed))
|
|
10905
|
+
return { rows: parsed };
|
|
10906
|
+
} catch {}
|
|
10907
|
+
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
10908
|
+
if (lines.length < 2) {
|
|
10909
|
+
console.error("文件至少要有表头 + 一行数据(TSV/CSV),或 JSON 行数组。");
|
|
10910
|
+
return EXIT_USAGE;
|
|
10911
|
+
}
|
|
10912
|
+
const delimiter = lines[0].includes("\t") ? "\t" : ",";
|
|
10913
|
+
const cells = lines.map((line) => line.split(delimiter).map((cell) => cell.trim().replace(/^"|"$/g, "")));
|
|
10914
|
+
const aliases = [
|
|
10915
|
+
["name", ["姓名", "名字", "name"]],
|
|
10916
|
+
["employeeNo", ["工号", "员工编号", "emp"]],
|
|
10917
|
+
["dept", ["部门", "dept"]],
|
|
10918
|
+
["role", ["岗位", "职位", "role"]],
|
|
10919
|
+
["hiredAt", ["入职", "hire"]],
|
|
10920
|
+
["probationEnd", ["试用期", "probation"]],
|
|
10921
|
+
["contractType", ["合同类型", "contract type"]],
|
|
10922
|
+
["contractEnd", ["合同到期", "合同终止", "contract end"]],
|
|
10923
|
+
["mobile", ["手机", "电话", "mobile", "phone"]],
|
|
10924
|
+
["email", ["邮箱", "email"]],
|
|
10925
|
+
["notes", ["备注", "note"]]
|
|
10926
|
+
];
|
|
10927
|
+
const mapping = (cells[0] ?? []).map((header) => {
|
|
10928
|
+
const lower = header.toLowerCase();
|
|
10929
|
+
for (const [field, list] of aliases)
|
|
10930
|
+
if (list.some((alias) => lower.includes(alias)))
|
|
10931
|
+
return field;
|
|
10932
|
+
return null;
|
|
10933
|
+
});
|
|
10934
|
+
const nameIndex = mapping.indexOf("name");
|
|
10935
|
+
const hireIndex = mapping.indexOf("hiredAt");
|
|
10936
|
+
if (nameIndex < 0 || hireIndex < 0) {
|
|
10937
|
+
console.error("表头里没认出「姓名」和「入职」列——JSON 数组格式不受此限。");
|
|
10938
|
+
return EXIT_USAGE;
|
|
10939
|
+
}
|
|
10940
|
+
const rows = [];
|
|
10941
|
+
for (const row of cells.slice(1)) {
|
|
10942
|
+
const record = {};
|
|
10943
|
+
let skip = false;
|
|
10944
|
+
mapping.forEach((field, index) => {
|
|
10945
|
+
if (!field)
|
|
10946
|
+
return;
|
|
10947
|
+
const value = (row[index] ?? "").trim();
|
|
10948
|
+
if (!value)
|
|
10949
|
+
return;
|
|
10950
|
+
if (field === "hiredAt" || field === "probationEnd" || field === "contractEnd") {
|
|
10951
|
+
const [y, m, d] = value.replace(/[/.]/g, "-").split("-").map(Number);
|
|
10952
|
+
const ms = y && m && d ? new Date(y, m - 1, d).getTime() : NaN;
|
|
10953
|
+
if (!Number.isFinite(ms)) {
|
|
10954
|
+
skip = true;
|
|
10955
|
+
return;
|
|
10956
|
+
}
|
|
10957
|
+
record[field] = ms;
|
|
10958
|
+
} else {
|
|
10959
|
+
record[field] = value;
|
|
10960
|
+
}
|
|
10961
|
+
});
|
|
10962
|
+
if (!skip && record.name && record.hiredAt)
|
|
10963
|
+
rows.push(record);
|
|
10964
|
+
}
|
|
10965
|
+
return { rows };
|
|
10966
|
+
}
|
|
10967
|
+
async function hrImport() {
|
|
10968
|
+
const file = positionalArgs(args.slice(2), HR_VALUED_FLAGS)[0];
|
|
10969
|
+
if (!file) {
|
|
10970
|
+
console.error("用法:crab hr import <文件.csv|.tsv|.json> [--org <arg>] [--json](≤200 行;CSV/TSV 首行表头,姓名与入职列必填)");
|
|
10971
|
+
return EXIT_USAGE;
|
|
10972
|
+
}
|
|
10973
|
+
let text;
|
|
10974
|
+
try {
|
|
10975
|
+
text = readFileSync2(file, "utf8");
|
|
10976
|
+
} catch {
|
|
10977
|
+
console.error(`读取文件失败:${file}`);
|
|
10978
|
+
return EXIT_USAGE;
|
|
10979
|
+
}
|
|
10980
|
+
const parsed = parseHrImportFile(text);
|
|
10981
|
+
if (typeof parsed === "number")
|
|
10982
|
+
return parsed;
|
|
10983
|
+
if (parsed.rows.length === 0) {
|
|
10984
|
+
console.error("没有可导入的行(姓名/入职日期缺失或日期格式不对)。");
|
|
10985
|
+
return EXIT_USAGE;
|
|
10986
|
+
}
|
|
10987
|
+
const credentials = requireCredentials();
|
|
10988
|
+
if (typeof credentials === "number")
|
|
10989
|
+
return credentials;
|
|
10990
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
10991
|
+
if (typeof org === "number")
|
|
10992
|
+
return org;
|
|
10993
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/people/import`, {
|
|
10994
|
+
method: "POST",
|
|
10995
|
+
headers: authHeaders(credentials),
|
|
10996
|
+
body: JSON.stringify({ rows: parsed.rows.slice(0, 200) })
|
|
10997
|
+
});
|
|
10998
|
+
if (res.status !== 200) {
|
|
10999
|
+
console.error(errorMessage(res.body, "导入失败。"));
|
|
11000
|
+
return exitCodeForApiError(res.body);
|
|
11001
|
+
}
|
|
11002
|
+
const report = res.body.data.report;
|
|
11003
|
+
if (hasFlag("--json"))
|
|
11004
|
+
console.log(JSON.stringify(report, null, 2));
|
|
11005
|
+
else {
|
|
11006
|
+
console.log(`✓ 导入完成:新建 ${report.created} 人${parsed.rows.length > 200 ? `(仅前 200 行,共 ${parsed.rows.length} 行)` : ""}。`);
|
|
11007
|
+
if (report.failed.length)
|
|
11008
|
+
console.log(` 失败 ${report.failed.length} 行:#${report.failed.map((f) => f.row + 1).join(", #")}`);
|
|
11009
|
+
}
|
|
11010
|
+
return 0;
|
|
11011
|
+
}
|
|
11012
|
+
async function hrTasksList() {
|
|
11013
|
+
const credentials = requireCredentials();
|
|
11014
|
+
if (typeof credentials === "number")
|
|
11015
|
+
return credentials;
|
|
11016
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11017
|
+
if (typeof org === "number")
|
|
11018
|
+
return org;
|
|
11019
|
+
const status = readFlag("--status");
|
|
11020
|
+
if (status && !HR_TASK_STATUSES.includes(status)) {
|
|
11021
|
+
console.error(`未知状态:${status}
|
|
11022
|
+
可用:${HR_TASK_STATUSES.join(", ")}`);
|
|
11023
|
+
return EXIT_USAGE;
|
|
11024
|
+
}
|
|
11025
|
+
const params = new URLSearchParams;
|
|
11026
|
+
if (status)
|
|
11027
|
+
params.set("status", status);
|
|
11028
|
+
const kind = readFlag("--kind");
|
|
11029
|
+
if (kind)
|
|
11030
|
+
params.set("kind", kind);
|
|
11031
|
+
const limit = readFlag("--limit");
|
|
11032
|
+
if (limit)
|
|
11033
|
+
params.set("limit", limit);
|
|
11034
|
+
const offset = readFlag("--offset");
|
|
11035
|
+
if (offset)
|
|
11036
|
+
params.set("offset", offset);
|
|
11037
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/tasks?${params}`, { headers: authHeaders(credentials) });
|
|
11038
|
+
if (res.status !== 200) {
|
|
11039
|
+
console.error(errorMessage(res.body, "获取任务失败。"));
|
|
11040
|
+
return exitCodeForApiError(res.body);
|
|
11041
|
+
}
|
|
11042
|
+
const page = res.body.data;
|
|
11043
|
+
if (hasFlag("--json")) {
|
|
11044
|
+
console.log(JSON.stringify({ ...page, org: { id: org.id, name: org.name } }, null, 2));
|
|
11045
|
+
return 0;
|
|
11046
|
+
}
|
|
11047
|
+
console.log(`组织:${hrOrgToken(org)} · 共 ${page.total} 条`);
|
|
11048
|
+
for (const task of page.tasks) {
|
|
11049
|
+
const due = task.dueAt ? new Date(task.dueAt).toISOString().slice(0, 10) : "—";
|
|
11050
|
+
console.log(`[${task.status}] ${task.title} ${task.node || ""} (${task.kind} · 截止 ${due}) id: ${task.id}`);
|
|
11051
|
+
}
|
|
11052
|
+
if (page.tasks.length < page.total)
|
|
11053
|
+
console.log(`(还有更多:--offset ${(Number(offset ?? 0) || 0) + page.tasks.length})`);
|
|
11054
|
+
return 0;
|
|
11055
|
+
}
|
|
11056
|
+
async function hrTaskCreate() {
|
|
11057
|
+
const kind = readFlag("--kind");
|
|
11058
|
+
const title = readFlag("--title");
|
|
11059
|
+
if (!kind || !title) {
|
|
11060
|
+
console.error('用法:crab hr task create --kind <attendance|payroll|contract|recruit|meeting|asset|admin|...> --title "..." [--node 节点说明] [--detail 详情] [--due YYYY-MM-DD] [--status alert|confirm|queued|done] [--json]');
|
|
11061
|
+
return EXIT_USAGE;
|
|
11062
|
+
}
|
|
11063
|
+
const status = readFlag("--status");
|
|
11064
|
+
if (status && !HR_TASK_STATUSES.includes(status)) {
|
|
11065
|
+
console.error(`未知状态:${status}
|
|
11066
|
+
可用:${HR_TASK_STATUSES.join(", ")}`);
|
|
11067
|
+
return EXIT_USAGE;
|
|
11068
|
+
}
|
|
11069
|
+
const due = parseHrDate(readFlag("--due"));
|
|
11070
|
+
if (due === null) {
|
|
11071
|
+
console.error("--due 格式应为 YYYY-MM-DD。");
|
|
11072
|
+
return EXIT_USAGE;
|
|
11073
|
+
}
|
|
11074
|
+
const credentials = requireCredentials();
|
|
11075
|
+
if (typeof credentials === "number")
|
|
11076
|
+
return credentials;
|
|
11077
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11078
|
+
if (typeof org === "number")
|
|
11079
|
+
return org;
|
|
11080
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/tasks`, {
|
|
11081
|
+
method: "POST",
|
|
11082
|
+
headers: authHeaders(credentials),
|
|
11083
|
+
body: JSON.stringify({ kind, title, node: readFlag("--node") ?? "", detail: readFlag("--detail") ?? "", ...status ? { status } : {}, ...due != null ? { dueAt: due } : {} })
|
|
11084
|
+
});
|
|
11085
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
11086
|
+
console.error(errorMessage(res.body, "创建任务失败。"));
|
|
11087
|
+
return exitCodeForApiError(res.body);
|
|
11088
|
+
}
|
|
11089
|
+
const task = res.body.data.task;
|
|
11090
|
+
if (hasFlag("--json"))
|
|
11091
|
+
console.log(JSON.stringify(task, null, 2));
|
|
11092
|
+
else
|
|
11093
|
+
console.log(`✓ 已创建任务 ${task.title} · id: ${task.id}`);
|
|
11094
|
+
return 0;
|
|
11095
|
+
}
|
|
11096
|
+
async function hrTaskUpdate() {
|
|
11097
|
+
const target = positionalArgs(args.slice(3), HR_VALUED_FLAGS)[0];
|
|
11098
|
+
const status = readFlag("--status");
|
|
11099
|
+
if (!target || !status && readFlag("--node") === undefined && readFlag("--detail") === undefined && readFlag("--due") === undefined) {
|
|
11100
|
+
console.error("用法:crab hr task update <id> [--status alert|confirm|queued|done] [--node 节点说明] [--detail 详情] [--due YYYY-MM-DD] [--json]");
|
|
11101
|
+
return EXIT_USAGE;
|
|
11102
|
+
}
|
|
11103
|
+
if (status && !HR_TASK_STATUSES.includes(status)) {
|
|
11104
|
+
console.error(`未知状态:${status}
|
|
11105
|
+
可用:${HR_TASK_STATUSES.join(", ")}`);
|
|
11106
|
+
return EXIT_USAGE;
|
|
11107
|
+
}
|
|
11108
|
+
const due = parseHrDate(readFlag("--due"));
|
|
11109
|
+
if (due === null) {
|
|
11110
|
+
console.error("--due 格式应为 YYYY-MM-DD。");
|
|
11111
|
+
return EXIT_USAGE;
|
|
11112
|
+
}
|
|
11113
|
+
const credentials = requireCredentials();
|
|
11114
|
+
if (typeof credentials === "number")
|
|
11115
|
+
return credentials;
|
|
11116
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11117
|
+
if (typeof org === "number")
|
|
11118
|
+
return org;
|
|
11119
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/tasks/${encodeURIComponent(target)}`, {
|
|
11120
|
+
method: "PATCH",
|
|
11121
|
+
headers: authHeaders(credentials),
|
|
11122
|
+
body: JSON.stringify({ ...status ? { status } : {}, ...readFlag("--node") !== undefined ? { node: readFlag("--node") } : {}, ...readFlag("--detail") !== undefined ? { detail: readFlag("--detail") } : {}, ...due != null ? { dueAt: due } : {} })
|
|
11123
|
+
});
|
|
11124
|
+
if (res.status !== 200) {
|
|
11125
|
+
console.error(errorMessage(res.body, "更新任务失败。"));
|
|
11126
|
+
return exitCodeForApiError(res.body);
|
|
11127
|
+
}
|
|
11128
|
+
const task = res.body.data.task;
|
|
11129
|
+
if (hasFlag("--json"))
|
|
11130
|
+
console.log(JSON.stringify(task, null, 2));
|
|
11131
|
+
else
|
|
11132
|
+
console.log(`✓ 已更新 ${task.title}(${task.status})。`);
|
|
11133
|
+
return 0;
|
|
11134
|
+
}
|
|
11135
|
+
async function hrDesk() {
|
|
11136
|
+
const credentials = requireCredentials();
|
|
11137
|
+
if (typeof credentials === "number")
|
|
11138
|
+
return credentials;
|
|
11139
|
+
const org = await resolveHrOrg(credentials, readFlag("--org"));
|
|
11140
|
+
if (typeof org === "number")
|
|
11141
|
+
return org;
|
|
11142
|
+
const res = await api2(credentials.apiBase, `/v1/hr/orgs/${encodeURIComponent(org.id)}/desk`, { headers: authHeaders(credentials) });
|
|
11143
|
+
if (res.status !== 200) {
|
|
11144
|
+
console.error(errorMessage(res.body, "获取工作台失败。"));
|
|
11145
|
+
return exitCodeForApiError(res.body);
|
|
11146
|
+
}
|
|
11147
|
+
const desk = res.body.data;
|
|
11148
|
+
if (hasFlag("--json")) {
|
|
11149
|
+
console.log(JSON.stringify({ ...desk, org: { id: org.id, name: org.name } }, null, 2));
|
|
11150
|
+
return 0;
|
|
11151
|
+
}
|
|
11152
|
+
console.log(`组织:${hrOrgToken(org)} · 工作台`);
|
|
11153
|
+
console.log(`
|
|
11154
|
+
等你拍板:`);
|
|
11155
|
+
for (const task of desk.desk.pending)
|
|
11156
|
+
console.log(` [${task.status}] ${task.title} ${task.node || ""} id: ${task.id}`);
|
|
11157
|
+
if (!desk.desk.pending.length)
|
|
11158
|
+
console.log(" (空)");
|
|
11159
|
+
console.log(`
|
|
11160
|
+
助理在办:`);
|
|
11161
|
+
for (const task of desk.desk.running)
|
|
11162
|
+
console.log(` ${task.title} ${task.node || ""} id: ${task.id}`);
|
|
11163
|
+
if (!desk.desk.running.length)
|
|
11164
|
+
console.log(" (空)");
|
|
11165
|
+
console.log(`
|
|
11166
|
+
近期完成:`);
|
|
11167
|
+
for (const task of desk.desk.done)
|
|
11168
|
+
console.log(` ${task.title} ${task.node || ""}`);
|
|
11169
|
+
if (!desk.desk.done.length)
|
|
11170
|
+
console.log(" (空)");
|
|
11171
|
+
console.log(`
|
|
11172
|
+
替你盯着:`);
|
|
11173
|
+
for (const reminder of desk.desk.reminders)
|
|
11174
|
+
console.log(` ⚠ ${reminder.text}(${reminder.days < 0 ? `过期 ${-reminder.days} 天` : `剩 ${reminder.days} 天`})`);
|
|
11175
|
+
if (!desk.desk.reminders.length)
|
|
11176
|
+
console.log(" (无)");
|
|
11177
|
+
console.log(`
|
|
11178
|
+
最近动态:`);
|
|
11179
|
+
for (const event of desk.desk.recentEvents.slice(0, 8)) {
|
|
11180
|
+
const day = new Date(event.at).toISOString().slice(0, 10);
|
|
11181
|
+
console.log(` ${day} ${event.personName ? `${event.personName} · ` : ""}${event.note}`);
|
|
11182
|
+
}
|
|
11183
|
+
if (!desk.desk.recentEvents.length)
|
|
11184
|
+
console.log(" (无)");
|
|
11185
|
+
return 0;
|
|
11186
|
+
}
|
|
11187
|
+
switch (command) {
|
|
11188
|
+
case "init":
|
|
11189
|
+
process.exit(await init());
|
|
11190
|
+
case "login":
|
|
11191
|
+
process.exit(await login());
|
|
11192
|
+
case "whoami":
|
|
11193
|
+
process.exit(await whoami());
|
|
11194
|
+
case "tokens": {
|
|
11195
|
+
const sub = args[1];
|
|
11196
|
+
if (sub === "list")
|
|
11197
|
+
process.exit(await tokensList());
|
|
11198
|
+
if (sub === "revoke")
|
|
11199
|
+
process.exit(await tokensRevoke(args[2]));
|
|
11200
|
+
console.error(sub ? `未知子命令:tokens ${sub}
|
|
11201
|
+
` : `缺少子命令:tokens list | tokens revoke <id>
|
|
11202
|
+
`);
|
|
11203
|
+
console.log(usage);
|
|
11204
|
+
process.exit(EXIT_USAGE);
|
|
11205
|
+
}
|
|
11206
|
+
case "skills": {
|
|
11207
|
+
const sub = args[1];
|
|
11208
|
+
if (sub === "install")
|
|
11209
|
+
process.exit(installSkills(args.slice(2)));
|
|
11210
|
+
if (sub === "list")
|
|
11211
|
+
process.exit(listSkills());
|
|
11212
|
+
if (sub === "uninstall")
|
|
11213
|
+
process.exit(uninstallSkills(args.slice(2)));
|
|
11214
|
+
console.error(`用法:crab skills install | list | uninstall [--agent claude,zcode,codex,agents]
|
|
11215
|
+
`);
|
|
11216
|
+
process.exit(EXIT_USAGE);
|
|
11217
|
+
}
|
|
11218
|
+
case "mail": {
|
|
11219
|
+
const sub = args[1];
|
|
11220
|
+
if (sub === "list")
|
|
11221
|
+
process.exit(await mailList());
|
|
11222
|
+
if (sub === "read")
|
|
11223
|
+
process.exit(await mailRead(args[2]));
|
|
11224
|
+
if (sub === "unread")
|
|
11225
|
+
process.exit(await mailUnread(args[2]));
|
|
11226
|
+
if (sub === "search")
|
|
11227
|
+
process.exit(await mailSearch(args.slice(2).join(" ") || undefined));
|
|
11228
|
+
if (sub === "send")
|
|
11229
|
+
process.exit(await mailSend());
|
|
11230
|
+
if (sub === "archive" || sub === "trash" || sub === "delete")
|
|
11231
|
+
process.exit(await mailMove(sub, args[2]));
|
|
11232
|
+
if (sub === "folders")
|
|
11233
|
+
process.exit(await mailFoldersOverview());
|
|
11234
|
+
if (sub === "activity")
|
|
11235
|
+
process.exit(await mailActivity());
|
|
11236
|
+
console.error(sub ? `未知子命令:mail ${sub}
|
|
11237
|
+
` : `缺少子命令:mail list | read | unread | search | send | archive | trash | delete | folders | activity
|
|
11238
|
+
`);
|
|
11239
|
+
console.log(usage);
|
|
11240
|
+
process.exit(EXIT_USAGE);
|
|
11241
|
+
}
|
|
11242
|
+
case "contacts": {
|
|
11243
|
+
const sub = args[1];
|
|
11244
|
+
if (sub === "list")
|
|
11245
|
+
process.exit(await contactsList(args.slice(2).join(" ") || undefined));
|
|
11246
|
+
if (sub === "add")
|
|
11247
|
+
process.exit(await contactsAdd(args[2]));
|
|
11248
|
+
if (sub === "rm")
|
|
11249
|
+
process.exit(await contactsRm(args[2]));
|
|
11250
|
+
if (sub === "invite")
|
|
11251
|
+
process.exit(await contactsInvite(hasFlag("--reset")));
|
|
11252
|
+
if (sub === "invitees")
|
|
11253
|
+
process.exit(await contactsInvitees());
|
|
11254
|
+
console.error(sub ? `未知子命令:contacts ${sub}
|
|
11255
|
+
` : `缺少子命令:contacts list | add | rm | invite | invitees
|
|
11256
|
+
`);
|
|
11257
|
+
console.log(usage);
|
|
11258
|
+
process.exit(EXIT_USAGE);
|
|
11259
|
+
}
|
|
11260
|
+
case "storage": {
|
|
11261
|
+
const sub = args[1];
|
|
11262
|
+
if (sub === "ls")
|
|
11263
|
+
process.exit(await storageList());
|
|
11264
|
+
if (sub === "upload")
|
|
11265
|
+
process.exit(await storageUpload(args.slice(2)));
|
|
11266
|
+
if (sub === "download")
|
|
11267
|
+
process.exit(await storageDownload(args[2]));
|
|
11268
|
+
if (sub === "rename")
|
|
11269
|
+
process.exit(await storageRename(args[2], args[3]));
|
|
11270
|
+
if (sub === "tag")
|
|
11271
|
+
process.exit(await storageTag(args[2]));
|
|
11272
|
+
if (sub === "category")
|
|
11273
|
+
process.exit(await storageCategory(args[2], args[3]));
|
|
11274
|
+
if (sub === "publish")
|
|
11275
|
+
process.exit(await storagePublish(args[2], true));
|
|
11276
|
+
if (sub === "unpublish")
|
|
11277
|
+
process.exit(await storagePublish(args[2], false));
|
|
11278
|
+
if (sub === "rm" || sub === "restore" || sub === "purge")
|
|
11279
|
+
process.exit(await storageMove(sub, args[2]));
|
|
11280
|
+
if (sub === "quota")
|
|
11281
|
+
process.exit(await storageQuota());
|
|
11282
|
+
if (sub === "categories")
|
|
11283
|
+
process.exit(await storageCategories());
|
|
11284
|
+
if (sub === "mkcat")
|
|
11285
|
+
process.exit(await storageMkCat(args.slice(2)));
|
|
11286
|
+
if (sub === "rencat")
|
|
11287
|
+
process.exit(await storageRenCat(args[2], args.slice(3)));
|
|
11288
|
+
if (sub === "rmcat")
|
|
11289
|
+
process.exit(await storageRmCat(args[2]));
|
|
11290
|
+
if (sub === "tags")
|
|
11291
|
+
process.exit(await storageTags());
|
|
11292
|
+
if (sub === "share")
|
|
11293
|
+
process.exit(await storageShareCmd(args[2]));
|
|
11294
|
+
if (sub === "shares")
|
|
11295
|
+
process.exit(await storageShares(args[2]));
|
|
11296
|
+
if (sub === "unshare")
|
|
11297
|
+
process.exit(await storageUnshare(args[2]));
|
|
11298
|
+
if (sub === "key")
|
|
11299
|
+
process.exit(await storageKeyCmd());
|
|
11300
|
+
if (sub === "activity")
|
|
11301
|
+
process.exit(await storageActivity());
|
|
11302
|
+
console.error(sub ? `未知子命令:storage ${sub}
|
|
11303
|
+
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | rencat | rmcat | tags | share | shares | unshare | key | activity
|
|
11304
|
+
`);
|
|
11305
|
+
console.log(usage);
|
|
11306
|
+
process.exit(EXIT_USAGE);
|
|
11307
|
+
}
|
|
11308
|
+
case "agent": {
|
|
11309
|
+
const sub = args[1];
|
|
11310
|
+
if (sub === "status")
|
|
11311
|
+
process.exit(await agentStatus());
|
|
11312
|
+
if (sub === "sessions")
|
|
11313
|
+
process.exit(await agentSessions());
|
|
11314
|
+
if (sub === "approvals")
|
|
11315
|
+
process.exit(await agentApprovals());
|
|
11316
|
+
console.error(sub ? `未知子命令:agent ${sub}
|
|
11317
|
+
` : `缺少子命令:agent status | sessions | approvals
|
|
11318
|
+
`);
|
|
11319
|
+
console.log(usage);
|
|
11320
|
+
process.exit(EXIT_USAGE);
|
|
11321
|
+
}
|
|
11322
|
+
case "shop": {
|
|
11323
|
+
const sub = args[1];
|
|
11324
|
+
if (sub === "status")
|
|
11325
|
+
process.exit(await shopStatus());
|
|
11326
|
+
if (sub === "roles")
|
|
11327
|
+
process.exit(await shopRoles());
|
|
11328
|
+
if (sub === "members")
|
|
11329
|
+
process.exit(await shopMembers());
|
|
11330
|
+
if (sub === "member") {
|
|
11331
|
+
if (args[2] === "edit")
|
|
11332
|
+
process.exit(await shopMemberEdit());
|
|
11333
|
+
console.error(`用法:crab shop member edit <用户名> --perms <a,b,c> [--biz sales,support] --store <slug>
|
|
11334
|
+
`);
|
|
11335
|
+
process.exit(EXIT_USAGE);
|
|
11336
|
+
}
|
|
11337
|
+
if (sub === "categories")
|
|
11338
|
+
process.exit(await shopCategories());
|
|
11339
|
+
if (sub === "category") {
|
|
11340
|
+
if (args[2] === "add")
|
|
11341
|
+
process.exit(await shopCategoryAdd());
|
|
11342
|
+
if (args[2] === "rename")
|
|
11343
|
+
process.exit(await shopCategoryRename());
|
|
11344
|
+
if (args[2] === "move")
|
|
11345
|
+
process.exit(await shopCategoryMove());
|
|
11346
|
+
if (args[2] === "delete")
|
|
11347
|
+
process.exit(await shopCategoryDelete());
|
|
11348
|
+
console.error(`用法:crab shop category add|rename|move|delete …(crab --help 看完整用法)
|
|
11349
|
+
`);
|
|
11350
|
+
process.exit(EXIT_USAGE);
|
|
11351
|
+
}
|
|
11352
|
+
if (sub === "specs")
|
|
11353
|
+
process.exit(await shopSpecs());
|
|
11354
|
+
if (sub === "spec") {
|
|
11355
|
+
if (args[2] === "add")
|
|
11356
|
+
process.exit(await shopSpecAdd());
|
|
11357
|
+
if (args[2] === "update")
|
|
11358
|
+
process.exit(await shopSpecUpdate());
|
|
11359
|
+
if (args[2] === "delete")
|
|
11360
|
+
process.exit(await shopSpecDelete());
|
|
11361
|
+
console.error(`用法:crab shop spec add|update|delete …(crab --help 看完整用法)
|
|
11362
|
+
`);
|
|
11363
|
+
process.exit(EXIT_USAGE);
|
|
11364
|
+
}
|
|
11365
|
+
if (sub === "invite")
|
|
11366
|
+
process.exit(await shopInvite());
|
|
11367
|
+
if (sub === "use") {
|
|
11368
|
+
const target = args.slice(2).find((x) => !x.startsWith("--"));
|
|
11369
|
+
process.exit(await shopUse(target));
|
|
11370
|
+
}
|
|
11371
|
+
if (sub === "apply") {
|
|
11372
|
+
const positional = [];
|
|
11373
|
+
const rest = args.slice(2);
|
|
11374
|
+
for (let i = 0;i < rest.length; i++) {
|
|
11375
|
+
if (rest[i] === "--tagline") {
|
|
11376
|
+
i++;
|
|
11377
|
+
continue;
|
|
11378
|
+
}
|
|
11379
|
+
if (rest[i]?.startsWith("--"))
|
|
11380
|
+
continue;
|
|
11381
|
+
positional.push(rest[i]);
|
|
11382
|
+
}
|
|
11383
|
+
const taglineArg = readFlag("--tagline");
|
|
11384
|
+
if (positional.length === 0) {
|
|
11385
|
+
console.error(`用法:crab shop apply <店铺名> [--tagline 一句话介绍] [--json]
|
|
11386
|
+
`);
|
|
11387
|
+
process.exit(EXIT_USAGE);
|
|
11388
|
+
}
|
|
11389
|
+
process.exit(await shopApply(positional.join(" "), taglineArg));
|
|
11390
|
+
}
|
|
11391
|
+
console.error(sub ? `未知子命令:shop ${sub}
|
|
11392
|
+
` : `缺少子命令:shop status | shop apply <店铺名>
|
|
11393
|
+
`);
|
|
11394
|
+
console.log(usage);
|
|
11395
|
+
process.exit(EXIT_USAGE);
|
|
11396
|
+
}
|
|
11397
|
+
case "credits": {
|
|
11398
|
+
const sub = args[1];
|
|
11399
|
+
if (!sub || sub === "balance")
|
|
11400
|
+
process.exit(await creditsBalance());
|
|
11401
|
+
if (sub === "history")
|
|
11402
|
+
process.exit(await creditsHistory());
|
|
11403
|
+
if (sub === "pricing")
|
|
11404
|
+
process.exit(await creditsPricing());
|
|
11405
|
+
console.error(`未知子命令:credits ${sub}
|
|
11406
|
+
`);
|
|
11407
|
+
console.log(usage);
|
|
11408
|
+
process.exit(EXIT_USAGE);
|
|
11409
|
+
}
|
|
11410
|
+
case "collab": {
|
|
11411
|
+
const sub = args[1];
|
|
11412
|
+
if (sub === "spaces")
|
|
11413
|
+
process.exit(await collabSpacesList());
|
|
11414
|
+
if (sub === "space") {
|
|
11415
|
+
if (args[2] === "create")
|
|
11416
|
+
process.exit(await collabSpaceCreate(args.slice(3).find((x) => !x.startsWith("--"))));
|
|
11417
|
+
console.error(`用法:crab collab space create <名称> [--allocation-mb <兆字节>] [--json]
|
|
11418
|
+
`);
|
|
11419
|
+
process.exit(EXIT_USAGE);
|
|
11420
|
+
}
|
|
11421
|
+
if (sub === "tasks")
|
|
11422
|
+
process.exit(await collabTasksList());
|
|
11423
|
+
if (sub === "task") {
|
|
11424
|
+
const op = args[2];
|
|
11425
|
+
if (op === "create")
|
|
11426
|
+
process.exit(await collabTaskCreate());
|
|
11427
|
+
if (op === "records")
|
|
11428
|
+
process.exit(await collabTaskRecords(args[3]));
|
|
11429
|
+
if (op === "done" || op === "reopen")
|
|
11430
|
+
process.exit(await collabTaskStatus(op, args[3]));
|
|
11431
|
+
if (op === "summary")
|
|
11432
|
+
process.exit(await collabTaskSummary(args[3]));
|
|
11433
|
+
process.exit(await collabTaskDetail(op));
|
|
11434
|
+
}
|
|
11435
|
+
if (sub === "members")
|
|
11436
|
+
process.exit(await collabMembersList());
|
|
11437
|
+
if (sub === "invite")
|
|
11438
|
+
process.exit(await collabInvite(args[2]));
|
|
11439
|
+
if (sub === "activity")
|
|
11440
|
+
process.exit(await collabActivity());
|
|
11441
|
+
console.error(sub ? `未知子命令:collab ${sub}
|
|
11442
|
+
` : `缺少子命令:collab spaces | space create | tasks | task | members | invite | activity
|
|
11443
|
+
`);
|
|
11444
|
+
console.log(usage);
|
|
11445
|
+
process.exit(EXIT_USAGE);
|
|
11446
|
+
}
|
|
11447
|
+
case "post":
|
|
11448
|
+
process.exit(await collabPushRecord(args[1]));
|
|
11449
|
+
case "hr": {
|
|
11450
|
+
const sub = args[1];
|
|
11451
|
+
if (sub === "orgs")
|
|
11452
|
+
process.exit(await hrOrgsList());
|
|
11453
|
+
if (sub === "org") {
|
|
11454
|
+
if (args[2] === "create")
|
|
11455
|
+
process.exit(await hrOrgCreate(args.slice(3).find((x) => !x.startsWith("--"))));
|
|
11456
|
+
console.error(`用法:crab hr org create <名称> [--json]
|
|
11457
|
+
`);
|
|
11458
|
+
process.exit(EXIT_USAGE);
|
|
11459
|
+
}
|
|
11460
|
+
if (sub === "members")
|
|
11461
|
+
process.exit(await hrMembersList());
|
|
11462
|
+
if (sub === "member") {
|
|
11463
|
+
const op = args[2];
|
|
11464
|
+
if (op === "add")
|
|
11465
|
+
process.exit(await hrMemberAdd());
|
|
11466
|
+
if (op === "set")
|
|
11467
|
+
process.exit(await hrMemberSet());
|
|
11468
|
+
if (op === "rm")
|
|
11469
|
+
process.exit(await hrMemberRm());
|
|
11470
|
+
console.error(`用法:crab hr member add|set|rm
|
|
11471
|
+
`);
|
|
11472
|
+
process.exit(EXIT_USAGE);
|
|
11473
|
+
}
|
|
11474
|
+
if (sub === "invite")
|
|
11475
|
+
process.exit(await hrInvite());
|
|
11476
|
+
if (sub === "people")
|
|
11477
|
+
process.exit(await hrPeopleList());
|
|
11478
|
+
if (sub === "person") {
|
|
11479
|
+
const op = args[2];
|
|
11480
|
+
if (op === "create")
|
|
11481
|
+
process.exit(await hrPersonCreate());
|
|
11482
|
+
if (op === "edit")
|
|
11483
|
+
process.exit(await hrPersonEdit());
|
|
11484
|
+
if (op === "offboard")
|
|
11485
|
+
process.exit(await hrPersonOffboard());
|
|
11486
|
+
process.exit(await hrPersonDetail(op));
|
|
11487
|
+
}
|
|
11488
|
+
if (sub === "event") {
|
|
11489
|
+
if (args[2] === "add")
|
|
11490
|
+
process.exit(await hrEventAdd());
|
|
11491
|
+
console.error(`用法:crab hr event add <personId> --type <类型> --note "..."
|
|
11492
|
+
`);
|
|
11493
|
+
process.exit(EXIT_USAGE);
|
|
11494
|
+
}
|
|
11495
|
+
if (sub === "events")
|
|
11496
|
+
process.exit(await hrEventsList());
|
|
11497
|
+
if (sub === "import")
|
|
11498
|
+
process.exit(await hrImport());
|
|
11499
|
+
if (sub === "tasks")
|
|
11500
|
+
process.exit(await hrTasksList());
|
|
11501
|
+
if (sub === "task") {
|
|
11502
|
+
const op = args[2];
|
|
11503
|
+
if (op === "create")
|
|
11504
|
+
process.exit(await hrTaskCreate());
|
|
11505
|
+
if (op === "update")
|
|
11506
|
+
process.exit(await hrTaskUpdate());
|
|
11507
|
+
console.error(`用法:crab hr task create|update
|
|
11508
|
+
`);
|
|
11509
|
+
process.exit(EXIT_USAGE);
|
|
11510
|
+
}
|
|
11511
|
+
if (sub === "desk")
|
|
11512
|
+
process.exit(await hrDesk());
|
|
11513
|
+
console.error(sub ? `未知子命令:hr ${sub}
|
|
11514
|
+
` : `缺少子命令:hr orgs | org create | members | member | invite | people | person | event add | events | import | tasks | task | desk
|
|
11515
|
+
`);
|
|
11516
|
+
console.log(usage);
|
|
11517
|
+
process.exit(EXIT_USAGE);
|
|
9353
11518
|
}
|
|
9354
|
-
case "post":
|
|
9355
|
-
process.exit(await collabPushRecord(args[1]));
|
|
9356
11519
|
case "logout":
|
|
9357
11520
|
process.exit(await logout());
|
|
9358
11521
|
case "version":
|