@crabcloud/cli 0.3.0 → 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 +2731 -164
- package/dist/skills/crabcloud-agent/SKILL.md +58 -0
- package/dist/skills/crabcloud-collab/SKILL.md +2 -0
- package/dist/skills/crabcloud-hr/SKILL.md +60 -0
- package/dist/skills/crabcloud-mail/SKILL.md +3 -2
- package/dist/skills/crabcloud-shop/SKILL.md +59 -0
- package/dist/skills/crabcloud-storage/SKILL.md +25 -16
- package/package.json +4 -4
package/dist/crab.js
CHANGED
|
@@ -2390,6 +2390,89 @@ var $ZodUnion = /* @__PURE__ */ $constructor("$ZodUnion", (inst, def) => {
|
|
|
2390
2390
|
});
|
|
2391
2391
|
};
|
|
2392
2392
|
});
|
|
2393
|
+
function discriminatorMap(def) {
|
|
2394
|
+
const map = new Map;
|
|
2395
|
+
for (const option of def.options) {
|
|
2396
|
+
const values = option._zod.propValues?.[def.discriminator];
|
|
2397
|
+
if (!values || values.size === 0)
|
|
2398
|
+
throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
|
|
2399
|
+
for (const value of values) {
|
|
2400
|
+
if (map.has(value)) {
|
|
2401
|
+
if (value !== undefined)
|
|
2402
|
+
throw new Error(`Duplicate discriminator value "${String(value)}"`);
|
|
2403
|
+
map.set(value, null);
|
|
2404
|
+
} else {
|
|
2405
|
+
map.set(value, option);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return map;
|
|
2410
|
+
}
|
|
2411
|
+
var $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnion", (inst, def) => {
|
|
2412
|
+
def.inclusive = false;
|
|
2413
|
+
$ZodUnion.init(inst, def);
|
|
2414
|
+
const _super = inst._zod.parse;
|
|
2415
|
+
defineLazyInternal(inst, "propValues", (zod) => {
|
|
2416
|
+
const propValues = {};
|
|
2417
|
+
let undefinedCount = 0;
|
|
2418
|
+
for (const option of zod.def.options) {
|
|
2419
|
+
const pv = option._zod.propValues;
|
|
2420
|
+
if (!pv || Object.keys(pv).length === 0)
|
|
2421
|
+
throw new Error(`Invalid discriminated union option at index "${zod.def.options.indexOf(option)}"`);
|
|
2422
|
+
if (pv[zod.def.discriminator]?.has(undefined))
|
|
2423
|
+
undefinedCount++;
|
|
2424
|
+
for (const [k, v] of Object.entries(pv)) {
|
|
2425
|
+
if (!Object.prototype.hasOwnProperty.call(propValues, k)) {
|
|
2426
|
+
assignProp(propValues, k, new Set);
|
|
2427
|
+
}
|
|
2428
|
+
for (const val of v) {
|
|
2429
|
+
propValues[k].add(val);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
if (!zod.def.unionFallback && undefinedCount > 1)
|
|
2434
|
+
propValues[zod.def.discriminator]?.delete(undefined);
|
|
2435
|
+
return propValues;
|
|
2436
|
+
});
|
|
2437
|
+
def.options.forEach((option, i) => {
|
|
2438
|
+
const propShape = rawShape(option._zod.def);
|
|
2439
|
+
if (propShape && !Object.prototype.hasOwnProperty.call(propShape, def.discriminator)) {
|
|
2440
|
+
throw new Error(`Invalid discriminated union option at index "${i}"`);
|
|
2441
|
+
}
|
|
2442
|
+
});
|
|
2443
|
+
const disc = cached(() => discriminatorMap(def));
|
|
2444
|
+
inst._zod.parse = (payload, ctx) => {
|
|
2445
|
+
const input = payload.value;
|
|
2446
|
+
if (!isObject(input)) {
|
|
2447
|
+
payload.issues.push({
|
|
2448
|
+
code: "invalid_type",
|
|
2449
|
+
expected: "object",
|
|
2450
|
+
input,
|
|
2451
|
+
inst
|
|
2452
|
+
});
|
|
2453
|
+
return payload;
|
|
2454
|
+
}
|
|
2455
|
+
const value = input?.[def.discriminator];
|
|
2456
|
+
const opt = disc.value.get(value);
|
|
2457
|
+
if (opt && (value !== undefined || ctx.direction !== "backward")) {
|
|
2458
|
+
return opt._zod.run(payload, ctx);
|
|
2459
|
+
}
|
|
2460
|
+
if (def.unionFallback || ctx.direction === "backward") {
|
|
2461
|
+
return _super(payload, ctx);
|
|
2462
|
+
}
|
|
2463
|
+
payload.issues.push({
|
|
2464
|
+
code: "invalid_union",
|
|
2465
|
+
errors: [],
|
|
2466
|
+
note: "No matching discriminator",
|
|
2467
|
+
discriminator: def.discriminator,
|
|
2468
|
+
options: Array.from(disc.value.keys()).filter((value) => disc.value.get(value) !== null),
|
|
2469
|
+
input,
|
|
2470
|
+
path: [def.discriminator],
|
|
2471
|
+
inst
|
|
2472
|
+
});
|
|
2473
|
+
return payload;
|
|
2474
|
+
};
|
|
2475
|
+
});
|
|
2393
2476
|
var $ZodIntersection = /* @__PURE__ */ $constructor("$ZodIntersection", (inst, def) => {
|
|
2394
2477
|
$ZodType.init(inst, def);
|
|
2395
2478
|
inst._zod.parse = (payload, ctx) => {
|
|
@@ -2682,6 +2765,28 @@ var $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
|
|
|
2682
2765
|
return payload;
|
|
2683
2766
|
};
|
|
2684
2767
|
});
|
|
2768
|
+
var $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
|
|
2769
|
+
$ZodType.init(inst, def);
|
|
2770
|
+
const values = new Set(def.values);
|
|
2771
|
+
inst._zod.values = values;
|
|
2772
|
+
defineLazyInternal(inst, "pattern", (zod) => {
|
|
2773
|
+
const vals = zod.def.values;
|
|
2774
|
+
return new RegExp(vals.length ? `^(${vals.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$` : "^[^\\s\\S]$");
|
|
2775
|
+
});
|
|
2776
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
2777
|
+
const input = payload.value;
|
|
2778
|
+
if (values.has(input)) {
|
|
2779
|
+
return payload;
|
|
2780
|
+
}
|
|
2781
|
+
payload.issues.push({
|
|
2782
|
+
code: "invalid_value",
|
|
2783
|
+
values: def.values,
|
|
2784
|
+
input,
|
|
2785
|
+
inst
|
|
2786
|
+
});
|
|
2787
|
+
return payload;
|
|
2788
|
+
};
|
|
2789
|
+
});
|
|
2685
2790
|
var $ZodTransform = /* @__PURE__ */ $constructor("$ZodTransform", (inst, def) => {
|
|
2686
2791
|
$ZodType.init(inst, def);
|
|
2687
2792
|
inst._zod.optin = "optional";
|
|
@@ -2883,6 +2988,9 @@ function handlePipeResult(left, next, ctx) {
|
|
|
2883
2988
|
}
|
|
2884
2989
|
return next._zod.run({ value: left.value, issues: left.issues }, ctx);
|
|
2885
2990
|
}
|
|
2991
|
+
var $ZodPreprocess = /* @__PURE__ */ $constructor("$ZodPreprocess", (inst, def) => {
|
|
2992
|
+
$ZodPipe.init(inst, def);
|
|
2993
|
+
});
|
|
2886
2994
|
var $ZodReadonly = /* @__PURE__ */ $constructor("$ZodReadonly", (inst, def) => {
|
|
2887
2995
|
$ZodType.init(inst, def);
|
|
2888
2996
|
defineLazyInternal(inst, "propValues", (zod) => zod.def.innerType._zod.propValues);
|
|
@@ -4587,6 +4695,45 @@ var enumProcessor = (schema, _ctx, json, _params) => {
|
|
|
4587
4695
|
json.type = "string";
|
|
4588
4696
|
json.enum = values;
|
|
4589
4697
|
};
|
|
4698
|
+
var literalProcessor = (schema, ctx, json, params) => {
|
|
4699
|
+
const def = schema._zod.def;
|
|
4700
|
+
if (def.values.length === 0) {
|
|
4701
|
+
json.not = {};
|
|
4702
|
+
return;
|
|
4703
|
+
}
|
|
4704
|
+
const vals = [];
|
|
4705
|
+
for (const val of def.values) {
|
|
4706
|
+
if (val === undefined) {
|
|
4707
|
+
if (handleUnrepresentable(schema, ctx, json, params, "Literal `undefined` cannot be represented in JSON Schema"))
|
|
4708
|
+
return;
|
|
4709
|
+
} else if (typeof val === "bigint") {
|
|
4710
|
+
if (handleUnrepresentable(schema, ctx, json, params, "BigInt literals cannot be represented in JSON Schema"))
|
|
4711
|
+
return;
|
|
4712
|
+
vals.push(Number(val));
|
|
4713
|
+
} else {
|
|
4714
|
+
vals.push(val);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
if (vals.length === 0) {} else if (vals.length === 1) {
|
|
4718
|
+
const val = vals[0];
|
|
4719
|
+
json.type = val === null ? "null" : typeof val;
|
|
4720
|
+
if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") {
|
|
4721
|
+
json.enum = [val];
|
|
4722
|
+
} else {
|
|
4723
|
+
json.const = val;
|
|
4724
|
+
}
|
|
4725
|
+
} else {
|
|
4726
|
+
if (vals.every((v) => typeof v === "number"))
|
|
4727
|
+
json.type = "number";
|
|
4728
|
+
if (vals.every((v) => typeof v === "string"))
|
|
4729
|
+
json.type = "string";
|
|
4730
|
+
if (vals.every((v) => typeof v === "boolean"))
|
|
4731
|
+
json.type = "boolean";
|
|
4732
|
+
if (vals.every((v) => v === null))
|
|
4733
|
+
json.type = "null";
|
|
4734
|
+
json.enum = vals;
|
|
4735
|
+
}
|
|
4736
|
+
};
|
|
4590
4737
|
var customProcessor = (schema, ctx, json, params) => {
|
|
4591
4738
|
handleUnrepresentable(schema, ctx, json, params, "Custom types cannot be represented in JSON Schema");
|
|
4592
4739
|
};
|
|
@@ -5618,6 +5765,18 @@ function union(options, params) {
|
|
|
5618
5765
|
...normalizeParams(params)
|
|
5619
5766
|
});
|
|
5620
5767
|
}
|
|
5768
|
+
var ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("ZodDiscriminatedUnion", (inst, def) => {
|
|
5769
|
+
ZodUnion.init(inst, def);
|
|
5770
|
+
$ZodDiscriminatedUnion.init(inst, def);
|
|
5771
|
+
});
|
|
5772
|
+
function discriminatedUnion(discriminator, options, params) {
|
|
5773
|
+
return new ZodDiscriminatedUnion({
|
|
5774
|
+
type: "union",
|
|
5775
|
+
options,
|
|
5776
|
+
discriminator,
|
|
5777
|
+
...normalizeParams(params)
|
|
5778
|
+
});
|
|
5779
|
+
}
|
|
5621
5780
|
var ZodIntersection = /* @__PURE__ */ $constructor("ZodIntersection", (inst, def) => {
|
|
5622
5781
|
$ZodIntersection.init(inst, def);
|
|
5623
5782
|
ZodType.init(inst, def);
|
|
@@ -5700,6 +5859,27 @@ function _enum(values, params) {
|
|
|
5700
5859
|
...normalizeParams(params)
|
|
5701
5860
|
});
|
|
5702
5861
|
}
|
|
5862
|
+
var ZodLiteral = /* @__PURE__ */ $constructor("ZodLiteral", (inst, def) => {
|
|
5863
|
+
$ZodLiteral.init(inst, def);
|
|
5864
|
+
ZodType.init(inst, def);
|
|
5865
|
+
inst._zod.processJSONSchema = (ctx, json, params) => literalProcessor(inst, ctx, json, params);
|
|
5866
|
+
inst.values = new Set(def.values);
|
|
5867
|
+
Object.defineProperty(inst, "value", {
|
|
5868
|
+
get() {
|
|
5869
|
+
if (def.values.length > 1) {
|
|
5870
|
+
throw new Error("This schema contains multiple valid literal values. Use `.values` instead.");
|
|
5871
|
+
}
|
|
5872
|
+
return def.values[0];
|
|
5873
|
+
}
|
|
5874
|
+
});
|
|
5875
|
+
});
|
|
5876
|
+
function literal(value, params) {
|
|
5877
|
+
return new ZodLiteral({
|
|
5878
|
+
type: "literal",
|
|
5879
|
+
values: Array.isArray(value) ? value : [value],
|
|
5880
|
+
...normalizeParams(params)
|
|
5881
|
+
});
|
|
5882
|
+
}
|
|
5703
5883
|
var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
|
|
5704
5884
|
_ensureDefaultMemoizer();
|
|
5705
5885
|
$ZodTransform.init(inst, def);
|
|
@@ -5848,6 +6028,10 @@ function pipe(in_, out) {
|
|
|
5848
6028
|
out
|
|
5849
6029
|
});
|
|
5850
6030
|
}
|
|
6031
|
+
var ZodPreprocess = /* @__PURE__ */ $constructor("ZodPreprocess", (inst, def) => {
|
|
6032
|
+
ZodPipe.init(inst, def);
|
|
6033
|
+
$ZodPreprocess.init(inst, def);
|
|
6034
|
+
});
|
|
5851
6035
|
var ZodReadonly = /* @__PURE__ */ $constructor("ZodReadonly", (inst, def) => {
|
|
5852
6036
|
$ZodReadonly.init(inst, def);
|
|
5853
6037
|
ZodType.init(inst, def);
|
|
@@ -5871,6 +6055,13 @@ function refine(fn, _params = {}) {
|
|
|
5871
6055
|
function superRefine(fn, params) {
|
|
5872
6056
|
return _superRefine(fn, params);
|
|
5873
6057
|
}
|
|
6058
|
+
function preprocess(fn, schema) {
|
|
6059
|
+
return new ZodPreprocess({
|
|
6060
|
+
type: "pipe",
|
|
6061
|
+
in: transform(fn),
|
|
6062
|
+
out: schema
|
|
6063
|
+
});
|
|
6064
|
+
}
|
|
5874
6065
|
// ../../node_modules/.bun/zod@4.6.5/node_modules/zod/v4/classic/coerce.js
|
|
5875
6066
|
var exports_coerce = {};
|
|
5876
6067
|
__export(exports_coerce, {
|
|
@@ -5895,6 +6086,298 @@ function bigint2(params) {
|
|
|
5895
6086
|
function date2(params) {
|
|
5896
6087
|
return _coercedDate(ZodDate, params);
|
|
5897
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
|
+
});
|
|
5898
6381
|
// ../../packages/contracts/src/index.ts
|
|
5899
6382
|
var accountDomain = "crabcloud.cc";
|
|
5900
6383
|
var adminDomain = `admin.${accountDomain}`;
|
|
@@ -5916,14 +6399,20 @@ var scopes = [
|
|
|
5916
6399
|
"contacts.read",
|
|
5917
6400
|
"contacts.write",
|
|
5918
6401
|
"collab.read",
|
|
5919
|
-
"collab.write"
|
|
6402
|
+
"collab.write",
|
|
6403
|
+
"hr.read",
|
|
6404
|
+
"hr.write",
|
|
6405
|
+
"agent.read",
|
|
6406
|
+
"agent.write"
|
|
5920
6407
|
];
|
|
5921
6408
|
var scopeGroups = [
|
|
5922
6409
|
{ id: "account", scopes: ["account.read", "credits.read"] },
|
|
5923
6410
|
{ id: "mail", scopes: ["mail.read", "mail.send", "mail.search", "mail.delete"] },
|
|
5924
6411
|
{ id: "storage", scopes: ["storage.read", "storage.write", "storage.delete"] },
|
|
5925
6412
|
{ id: "collab", scopes: ["collab.read", "collab.write"] },
|
|
5926
|
-
{ id: "contacts", scopes: ["contacts.read", "contacts.write"] }
|
|
6413
|
+
{ id: "contacts", scopes: ["contacts.read", "contacts.write"] },
|
|
6414
|
+
{ id: "hr", scopes: ["hr.read", "hr.write"] },
|
|
6415
|
+
{ id: "agent", scopes: ["agent.read", "agent.write"] }
|
|
5927
6416
|
];
|
|
5928
6417
|
function expandScopes(input) {
|
|
5929
6418
|
const expanded = input.flatMap((item) => {
|
|
@@ -6005,7 +6494,8 @@ var sessionSummarySchema = object({
|
|
|
6005
6494
|
var deviceStartInput = object({
|
|
6006
6495
|
name: string2().trim().min(1).max(80),
|
|
6007
6496
|
scopes: array(_enum(scopes)).min(1).max(scopes.length),
|
|
6008
|
-
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()
|
|
6009
6499
|
});
|
|
6010
6500
|
var devicePollInput = object({ deviceCode: string2().min(16).max(200) });
|
|
6011
6501
|
var deviceDecisionInput = object({
|
|
@@ -6018,11 +6508,18 @@ var agentTokenSummarySchema = object({
|
|
|
6018
6508
|
name: string2(),
|
|
6019
6509
|
scopes: array(_enum(scopes)),
|
|
6020
6510
|
collabSpace: string2().nullable().optional(),
|
|
6511
|
+
hrOrg: string2().nullable().optional(),
|
|
6512
|
+
kind: _enum(["agent", "api"]).optional(),
|
|
6513
|
+
keyHint: string2().optional(),
|
|
6021
6514
|
createdAt: number2(),
|
|
6022
6515
|
lastUsedAt: number2().nullable(),
|
|
6023
6516
|
expiresAt: number2().nullable(),
|
|
6024
6517
|
revoked: boolean2()
|
|
6025
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
|
+
});
|
|
6026
6523
|
var oauthProviders = ["google", "reai", "github"];
|
|
6027
6524
|
var oauthUnbindInput = object({ provider: _enum(oauthProviders) });
|
|
6028
6525
|
var oauthOnboardingConfirmInput = object({
|
|
@@ -6069,6 +6566,12 @@ var adminPlanTierUpdateInput = object({
|
|
|
6069
6566
|
quotaJson: record(string2(), unknown()).optional(),
|
|
6070
6567
|
status: _enum(adminPlanTierStatuses).optional()
|
|
6071
6568
|
});
|
|
6569
|
+
var adminShopStoreStatusInput = object({
|
|
6570
|
+
status: _enum(["active", "archived"])
|
|
6571
|
+
});
|
|
6572
|
+
var adminShopApplicationRejectInput = object({
|
|
6573
|
+
note: string2().trim().min(1).max(500)
|
|
6574
|
+
});
|
|
6072
6575
|
var subscriptionChangeInput = object({
|
|
6073
6576
|
tierId: string2().trim().min(1).max(50)
|
|
6074
6577
|
});
|
|
@@ -6229,6 +6732,160 @@ var collabRecordListQuery = object({
|
|
|
6229
6732
|
cursor: string2().trim().max(200).optional(),
|
|
6230
6733
|
limit: exports_coerce.number().int().min(1).max(200).optional()
|
|
6231
6734
|
});
|
|
6735
|
+
var agentPermissionModes = ["confirm", "auto_edit", "yolo"];
|
|
6736
|
+
var agentSessionKinds = ["chat", "task"];
|
|
6737
|
+
var agentMemoryTypes = ["preference", "contact", "project", "daily"];
|
|
6738
|
+
var AGENT_TITLE_MAX = 120;
|
|
6739
|
+
var AGENT_MEMORY_MAX = 2000;
|
|
6740
|
+
var AGENT_PERSONA_MAX = 2000;
|
|
6741
|
+
var agentToolsConfigSchema = object({
|
|
6742
|
+
web: boolean2().optional(),
|
|
6743
|
+
code: boolean2().optional(),
|
|
6744
|
+
workspace: boolean2().optional(),
|
|
6745
|
+
crab: object({
|
|
6746
|
+
credits: boolean2().optional(),
|
|
6747
|
+
mail: boolean2().optional(),
|
|
6748
|
+
storage: boolean2().optional(),
|
|
6749
|
+
collab: boolean2().optional(),
|
|
6750
|
+
contacts: boolean2().optional()
|
|
6751
|
+
}).optional()
|
|
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
|
+
});
|
|
6759
|
+
var agentConfigUpdateInput = object({
|
|
6760
|
+
activeModelId: string2().trim().min(1).max(64).optional(),
|
|
6761
|
+
persona: string2().max(AGENT_PERSONA_MAX).optional(),
|
|
6762
|
+
mode: _enum(agentPermissionModes).optional(),
|
|
6763
|
+
planEnabled: boolean2().optional(),
|
|
6764
|
+
tools: agentToolsConfigSchema.optional()
|
|
6765
|
+
});
|
|
6766
|
+
var AGENT_PROVIDER_LABEL_MAX = 60;
|
|
6767
|
+
var agentProviderInput = object({
|
|
6768
|
+
label: string2().trim().min(1).max(AGENT_PROVIDER_LABEL_MAX),
|
|
6769
|
+
baseUrl: string2().trim().min(1).max(500),
|
|
6770
|
+
apiKey: string2().trim().min(1).max(500)
|
|
6771
|
+
});
|
|
6772
|
+
var agentProviderModelInput = object({
|
|
6773
|
+
model: string2().trim().min(1).max(200),
|
|
6774
|
+
contextWindow: number2().int().min(1000).max(1e8).nullable().optional(),
|
|
6775
|
+
maxOutput: number2().int().min(100).max(1e7).nullable().optional(),
|
|
6776
|
+
inputText: boolean2().optional(),
|
|
6777
|
+
inputImage: boolean2().optional(),
|
|
6778
|
+
inputVideo: boolean2().optional()
|
|
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
|
+
});
|
|
6785
|
+
var agentSessionCreateInput = object({
|
|
6786
|
+
kind: _enum(agentSessionKinds).default("chat"),
|
|
6787
|
+
title: string2().trim().max(AGENT_TITLE_MAX).optional(),
|
|
6788
|
+
repeatCron: string2().trim().max(100).optional(),
|
|
6789
|
+
notifyMail: boolean2().optional()
|
|
6790
|
+
}).refine((v) => v.kind === "chat" || !!v.title && v.title.length > 0, {
|
|
6791
|
+
message: "task requires title"
|
|
6792
|
+
});
|
|
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
|
+
});
|
|
6810
|
+
var agentSessionListQuery = object({
|
|
6811
|
+
kind: _enum(agentSessionKinds).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()
|
|
6822
|
+
});
|
|
6823
|
+
var agentMemoryCreateInput = object({
|
|
6824
|
+
type: _enum(agentMemoryTypes),
|
|
6825
|
+
content: string2().trim().min(1).max(AGENT_MEMORY_MAX),
|
|
6826
|
+
sessionId: string2().max(64).optional()
|
|
6827
|
+
});
|
|
6828
|
+
var agentTraceStatuses = ["running", "ok", "error", "pending", "approved", "rejected"];
|
|
6829
|
+
var agentPlanItemStates = ["done", "current", "todo"];
|
|
6830
|
+
var agentTraceCommon = {
|
|
6831
|
+
id: string2().min(1).max(64),
|
|
6832
|
+
status: _enum(agentTraceStatuses),
|
|
6833
|
+
round: number2().int().min(1),
|
|
6834
|
+
title: string2().max(200),
|
|
6835
|
+
durationMs: number2().int().min(0).optional()
|
|
6836
|
+
};
|
|
6837
|
+
var agentPlanTraceSchema = object({
|
|
6838
|
+
...agentTraceCommon,
|
|
6839
|
+
kind: literal("plan"),
|
|
6840
|
+
items: array(object({ text: string2().max(500), state: _enum(agentPlanItemStates) })).max(50)
|
|
6841
|
+
});
|
|
6842
|
+
var agentWebTraceSchema = object({
|
|
6843
|
+
...agentTraceCommon,
|
|
6844
|
+
kind: literal("web"),
|
|
6845
|
+
url: string2().max(2000),
|
|
6846
|
+
summary: string2().max(8000)
|
|
6847
|
+
});
|
|
6848
|
+
var agentArtifactSchema = object({
|
|
6849
|
+
name: string2().max(200),
|
|
6850
|
+
mime: string2().max(100),
|
|
6851
|
+
path: string2().max(500),
|
|
6852
|
+
rows: number2().int().min(0).optional()
|
|
6853
|
+
});
|
|
6854
|
+
var agentCodeTraceSchema = object({
|
|
6855
|
+
...agentTraceCommon,
|
|
6856
|
+
kind: literal("code"),
|
|
6857
|
+
code: string2().max(50000).optional(),
|
|
6858
|
+
result: string2().max(20000).optional(),
|
|
6859
|
+
artifact: agentArtifactSchema.optional()
|
|
6860
|
+
});
|
|
6861
|
+
var agentCrabTraceSchema = object({
|
|
6862
|
+
...agentTraceCommon,
|
|
6863
|
+
kind: literal("crab"),
|
|
6864
|
+
command: string2().max(2000),
|
|
6865
|
+
response: string2().max(8000).optional()
|
|
6866
|
+
});
|
|
6867
|
+
var agentApprovalTraceSchema = object({
|
|
6868
|
+
...agentTraceCommon,
|
|
6869
|
+
kind: literal("approval"),
|
|
6870
|
+
tool: string2().max(64),
|
|
6871
|
+
action: string2().max(200),
|
|
6872
|
+
detail: string2().max(8000)
|
|
6873
|
+
});
|
|
6874
|
+
var agentTraceSchema = discriminatedUnion("kind", [
|
|
6875
|
+
agentPlanTraceSchema,
|
|
6876
|
+
agentWebTraceSchema,
|
|
6877
|
+
agentCodeTraceSchema,
|
|
6878
|
+
agentCrabTraceSchema,
|
|
6879
|
+
agentApprovalTraceSchema
|
|
6880
|
+
]);
|
|
6881
|
+
var agentMemoryChipSchema = object({
|
|
6882
|
+
text: string2().max(200),
|
|
6883
|
+
memoryId: string2().max(64).optional()
|
|
6884
|
+
});
|
|
6885
|
+
var agentCompressionChipSchema = object({
|
|
6886
|
+
foldedRounds: number2().int().min(1),
|
|
6887
|
+
summary: string2().max(4000)
|
|
6888
|
+
});
|
|
6232
6889
|
|
|
6233
6890
|
// ../../packages/cloudkit/src/crypto.ts
|
|
6234
6891
|
var enc = new TextEncoder;
|
|
@@ -6519,6 +7176,9 @@ function exitCodeForApiError(body) {
|
|
|
6519
7176
|
case "collab_space_not_bound":
|
|
6520
7177
|
case "collab_space_mismatch":
|
|
6521
7178
|
return EXIT_SCOPE;
|
|
7179
|
+
case "hr_org_not_bound":
|
|
7180
|
+
case "hr_org_mismatch":
|
|
7181
|
+
return EXIT_SCOPE;
|
|
6522
7182
|
default:
|
|
6523
7183
|
return 1;
|
|
6524
7184
|
}
|
|
@@ -6598,13 +7258,15 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6598
7258
|
contacts invite [--reset] [--json] 我的邀请码、链接与统计(--reset 重置,旧链接失效)
|
|
6599
7259
|
contacts invitees [--offset N] [--json]
|
|
6600
7260
|
我邀请的人(直接下级 + 间接计数)
|
|
6601
|
-
storage ls [--q 关键字] [--source files|media|mail] [--category <id|none>]
|
|
7261
|
+
storage ls [--q 关键字] [--source files|media|mail|collab|agent] [--category <id|none>]
|
|
6602
7262
|
[--type doc|image|video|archive|code] [--tag a,b] [--trash]
|
|
6603
|
-
[--shared] [--recent] [--message-id <id>]
|
|
6604
|
-
|
|
7263
|
+
[--shared] [--recent] [--encrypted only|plain] [--message-id <id>]
|
|
7264
|
+
[--limit N] [--offset N] [--json]
|
|
7265
|
+
列出云盘对象(跨来源统一视图,能力与 Web 云盘一致;
|
|
7266
|
+
--encrypted only=仅保险箱、plain=排除保险箱)
|
|
6605
7267
|
storage upload <file...> [--json] 上传一个或多个文件(直传优先,自动回退中转);
|
|
6606
|
-
|
|
6607
|
-
CRAB_PASSPHRASE
|
|
7268
|
+
默认明文上传(2026-10 基础服务化);--passphrase /
|
|
7269
|
+
CRAB_PASSPHRASE 提供时端到端加密(保险箱文件)
|
|
6608
7270
|
storage download <id|文件名> [-o 输出路径]
|
|
6609
7271
|
下载对象内容(默认按原文件名存到当前目录;
|
|
6610
7272
|
加密对象需口令,终端内运行时交互输入)
|
|
@@ -6617,7 +7279,7 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6617
7279
|
公开 / 收回公开(公开时打印公开直链)
|
|
6618
7280
|
storage rm|restore|purge <id|文件名>
|
|
6619
7281
|
回收站 / 恢复 / 彻底删除(purge 不可恢复)
|
|
6620
|
-
storage quota [--json]
|
|
7282
|
+
storage quota [--json] 存储水位(统一一本账:五来源分项 + 保险箱计数)
|
|
6621
7283
|
storage share <id|文件名> [--expires 7|30|90|forever] [--passcode 提取码]
|
|
6622
7284
|
创建分享链接(加密文件链接自带解密钥匙 #fragment)
|
|
6623
7285
|
storage key [--remember|--forget]
|
|
@@ -6657,6 +7319,67 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6657
7319
|
collab activity [--json] 协作域活动事件(Agent 推送/读取、任务创建、摘要更新)
|
|
6658
7320
|
post <taskId> (--body "..." | --body-file <path>) [--attach <fileId,...>] [--link <url,...>] [--json]
|
|
6659
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
|
+
规格模板管理(引用 = 快照拷贝,改/删不影响已建商品)
|
|
6660
7383
|
credits [--json] 积分余额(月送/充值/合计;需 credits.read scope)
|
|
6661
7384
|
credits history [--limit N] [--offset N] [--json]
|
|
6662
7385
|
积分流水(变动 ±:消耗为负;需 credits.read)
|
|
@@ -6700,10 +7423,21 @@ async function login() {
|
|
|
6700
7423
|
console.error("示例:crab login --scopes collab --space default");
|
|
6701
7424
|
return EXIT_USAGE;
|
|
6702
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
|
+
}
|
|
6703
7432
|
const openBrowser = !hasFlag("--no-open");
|
|
6704
7433
|
const start = await api2(apiBase, "/v1/auth/device/start", {
|
|
6705
7434
|
method: "POST",
|
|
6706
|
-
body: JSON.stringify({
|
|
7435
|
+
body: JSON.stringify({
|
|
7436
|
+
name: `${hostname()} · crab cli`,
|
|
7437
|
+
scopes: requested,
|
|
7438
|
+
...spaceArg ? { space: spaceArg } : {},
|
|
7439
|
+
...orgArg ? { org: orgArg } : {}
|
|
7440
|
+
})
|
|
6707
7441
|
});
|
|
6708
7442
|
if (start.status !== 200) {
|
|
6709
7443
|
console.error(`发起授权失败(${start.status}):`, errorMessage(start.body, JSON.stringify(start.body)));
|
|
@@ -6747,6 +7481,8 @@ async function login() {
|
|
|
6747
7481
|
console.log(` scopes: ${poll.body.data.scopes.join(", ")}`);
|
|
6748
7482
|
if (spaceArg)
|
|
6749
7483
|
console.log(` 协作空间绑定: ${spaceArg}(collab 命令只能作用于该空间)`);
|
|
7484
|
+
if (orgArg)
|
|
7485
|
+
console.log(` HR 组织绑定: ${orgArg}(hr 命令只能作用于该组织)`);
|
|
6750
7486
|
return 0;
|
|
6751
7487
|
}
|
|
6752
7488
|
}
|
|
@@ -6889,11 +7625,13 @@ async function tokensList() {
|
|
|
6889
7625
|
return 0;
|
|
6890
7626
|
}
|
|
6891
7627
|
if (rows.length === 0) {
|
|
6892
|
-
console.log("
|
|
7628
|
+
console.log("还没有已授权的令牌。");
|
|
6893
7629
|
return 0;
|
|
6894
7630
|
}
|
|
6895
7631
|
for (const token of rows) {
|
|
6896
|
-
|
|
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}⟩` : ""}`);
|
|
6897
7635
|
console.log(` 创建 ${formatTime(token.createdAt)} · 最近使用 ${token.lastUsedAt ? formatTime(token.lastUsedAt) : "—"}${token.expiresAt ? ` · 过期 ${formatTime(token.expiresAt)}` : ""}`);
|
|
6898
7636
|
}
|
|
6899
7637
|
return 0;
|
|
@@ -7122,8 +7860,8 @@ async function mailSend() {
|
|
|
7122
7860
|
const hit = await resolveStorageObject(credentials, token);
|
|
7123
7861
|
if (typeof hit === "number")
|
|
7124
7862
|
return hit;
|
|
7125
|
-
if (typeof hit === "object" && hit.isEncrypted) {
|
|
7126
|
-
console.error(`「${token}
|
|
7863
|
+
if (typeof hit === "object" && hit.isEncrypted && hit.wrappedDataKey?.startsWith("u1.")) {
|
|
7864
|
+
console.error(`「${token}」是保险箱文件(端到端加密),不能作为邮件附件发送。`);
|
|
7127
7865
|
return EXIT_USAGE;
|
|
7128
7866
|
}
|
|
7129
7867
|
const id = typeof hit === "object" ? hit.id : hit;
|
|
@@ -7392,7 +8130,7 @@ async function contactsInvitees() {
|
|
|
7392
8130
|
console.log(`(还有更多:contacts invitees --offset ${page.nextOffset})`);
|
|
7393
8131
|
return 0;
|
|
7394
8132
|
}
|
|
7395
|
-
var STORAGE_SOURCES = ["files", "media", "mail"];
|
|
8133
|
+
var STORAGE_SOURCES = ["files", "media", "mail", "collab", "agent"];
|
|
7396
8134
|
var STORAGE_TYPES = ["doc", "image", "video", "archive", "code"];
|
|
7397
8135
|
var MIME_BY_EXT = {
|
|
7398
8136
|
png: "image/png",
|
|
@@ -7429,9 +8167,10 @@ function formatBytesCli(n) {
|
|
|
7429
8167
|
return `${n} B`;
|
|
7430
8168
|
}
|
|
7431
8169
|
function printStorageRow(o) {
|
|
8170
|
+
const encFlag = o.isEncrypted ? o.wrappedDataKey?.startsWith("u1.") ? "保险箱" : "平台加密" : null;
|
|
7432
8171
|
const flags = [
|
|
7433
|
-
o.source !== "files" ? o.source === "media" ? "素材" : "邮件附件" : null,
|
|
7434
|
-
|
|
8172
|
+
o.source !== "files" ? o.source === "media" ? "素材" : o.source === "mail" ? "邮件附件" : o.source === "collab" ? "协作" : "Agent 产物" : null,
|
|
8173
|
+
encFlag,
|
|
7435
8174
|
o.isPublic ? "公开" : null
|
|
7436
8175
|
].filter(Boolean);
|
|
7437
8176
|
console.log(`${o.id} ${o.filename} ${formatBytesCli(o.sizeBytes)} ${formatTime(o.createdAt)}${flags.length > 0 ? ` [${flags.join(" · ")}]` : ""}`);
|
|
@@ -7501,6 +8240,15 @@ async function storageList() {
|
|
|
7501
8240
|
const tags = readFlag("--tag");
|
|
7502
8241
|
if (tags)
|
|
7503
8242
|
params.set("tags", tags);
|
|
8243
|
+
const encrypted = readFlag("--encrypted");
|
|
8244
|
+
if (encrypted) {
|
|
8245
|
+
if (encrypted !== "only" && encrypted !== "plain") {
|
|
8246
|
+
console.error(`未知 encrypted 过滤:${encrypted}
|
|
8247
|
+
可用:only(仅保险箱)、plain(排除保险箱)`);
|
|
8248
|
+
return EXIT_USAGE;
|
|
8249
|
+
}
|
|
8250
|
+
params.set("encrypted", encrypted);
|
|
8251
|
+
}
|
|
7504
8252
|
if (hasFlag("--trash"))
|
|
7505
8253
|
params.set("trash", "1");
|
|
7506
8254
|
if (hasFlag("--shared"))
|
|
@@ -7759,7 +8507,7 @@ async function driveMasterKey(credentials) {
|
|
|
7759
8507
|
return raw;
|
|
7760
8508
|
return importAesKey(raw);
|
|
7761
8509
|
}
|
|
7762
|
-
async function storageUploadOne(credentials, filePath) {
|
|
8510
|
+
async function storageUploadOne(credentials, filePath, encrypt) {
|
|
7763
8511
|
let buf;
|
|
7764
8512
|
try {
|
|
7765
8513
|
buf = readFileSync2(filePath);
|
|
@@ -7769,22 +8517,26 @@ async function storageUploadOne(credentials, filePath) {
|
|
|
7769
8517
|
}
|
|
7770
8518
|
const filename = basename(filePath);
|
|
7771
8519
|
const mimeType = mimeFor(filename);
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
8520
|
+
let body = new Uint8Array(buf);
|
|
8521
|
+
let wrappedDataKey = null;
|
|
8522
|
+
if (encrypt) {
|
|
8523
|
+
const master = await driveMasterKey(credentials);
|
|
8524
|
+
if (typeof master === "number")
|
|
8525
|
+
return master;
|
|
8526
|
+
const dataKey = randomKeyBytes(32);
|
|
8527
|
+
const sealed = await aeadEncrypt(await importAesKey(dataKey), body, DATA_AAD);
|
|
8528
|
+
body = new Uint8Array([...sealed.iv, ...sealed.ciphertext]);
|
|
8529
|
+
const wrapped = await aeadEncrypt(master, dataKey, DATA_AAD);
|
|
8530
|
+
wrappedDataKey = `u1.${sealToB64url({ iv: wrapped.iv, ciphertext: wrapped.ciphertext })}`;
|
|
8531
|
+
}
|
|
7780
8532
|
const presign = await api2(credentials.apiBase, "/v1/storage/presign", {
|
|
7781
8533
|
method: "POST",
|
|
7782
8534
|
headers: authHeaders(credentials),
|
|
7783
|
-
body: JSON.stringify({ filename, mimeType, sizeBytes:
|
|
8535
|
+
body: JSON.stringify({ filename, mimeType, sizeBytes: body.byteLength, enc: wrappedDataKey !== null })
|
|
7784
8536
|
});
|
|
7785
8537
|
if (presign.status === 200 || presign.status === 201) {
|
|
7786
8538
|
const { fileId, uploadUrl } = presign.body.data;
|
|
7787
|
-
const put = await fetch(uploadUrl, { method: "PUT", body
|
|
8539
|
+
const put = await fetch(uploadUrl, { method: "PUT", body, headers: { "content-type": mimeType } });
|
|
7788
8540
|
if (!put.ok) {
|
|
7789
8541
|
console.error(`直传失败(${put.status}):${filename}`);
|
|
7790
8542
|
return 1;
|
|
@@ -7792,7 +8544,7 @@ async function storageUploadOne(credentials, filePath) {
|
|
|
7792
8544
|
const complete = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(fileId)}/complete`, {
|
|
7793
8545
|
method: "POST",
|
|
7794
8546
|
headers: authHeaders(credentials),
|
|
7795
|
-
body: JSON.stringify({ isEncrypted: true, wrappedDataKey })
|
|
8547
|
+
body: JSON.stringify(wrappedDataKey ? { isEncrypted: true, wrappedDataKey } : {})
|
|
7796
8548
|
});
|
|
7797
8549
|
if (complete.status !== 200 && complete.status !== 201) {
|
|
7798
8550
|
console.error(errorMessage(complete.body, `记账失败:${filename}`));
|
|
@@ -7802,28 +8554,30 @@ async function storageUploadOne(credentials, filePath) {
|
|
|
7802
8554
|
if (hasFlag("--json"))
|
|
7803
8555
|
console.log(JSON.stringify(object, null, 2));
|
|
7804
8556
|
else
|
|
7805
|
-
console.log(`✓
|
|
8557
|
+
console.log(`✓ 已上传${wrappedDataKey ? "(已加密)" : ""}:${object.filename}(id ${object.id})`);
|
|
7806
8558
|
return 0;
|
|
7807
8559
|
}
|
|
7808
8560
|
if (presign.body?.error?.code === "presign_unavailable") {
|
|
7809
8561
|
const form = new FormData;
|
|
7810
|
-
form.append("file", new Blob([
|
|
7811
|
-
|
|
7812
|
-
|
|
8562
|
+
form.append("file", new Blob([body], { type: mimeType }), filename);
|
|
8563
|
+
if (wrappedDataKey) {
|
|
8564
|
+
form.append("enc", "1");
|
|
8565
|
+
form.append("wrappedDataKey", wrappedDataKey);
|
|
8566
|
+
}
|
|
7813
8567
|
const res = await fetch(`${credentials.apiBase}/v1/storage`, {
|
|
7814
8568
|
method: "POST",
|
|
7815
8569
|
headers: authHeaders(credentials),
|
|
7816
8570
|
body: form
|
|
7817
8571
|
});
|
|
7818
|
-
const
|
|
8572
|
+
const body2 = await res.json().catch(() => ({}));
|
|
7819
8573
|
if (res.status !== 200 && res.status !== 201) {
|
|
7820
|
-
console.error(
|
|
7821
|
-
return exitCodeForApiError(
|
|
8574
|
+
console.error(body2.error?.message ?? `上传失败:${filename}`);
|
|
8575
|
+
return exitCodeForApiError(body2);
|
|
7822
8576
|
}
|
|
7823
8577
|
if (hasFlag("--json"))
|
|
7824
|
-
console.log(JSON.stringify(
|
|
8578
|
+
console.log(JSON.stringify(body2.data, null, 2));
|
|
7825
8579
|
else
|
|
7826
|
-
console.log(`✓
|
|
8580
|
+
console.log(`✓ 已上传${wrappedDataKey ? "(已加密)" : ""}:${body2.data?.filename}(id ${body2.data?.id})`);
|
|
7827
8581
|
return 0;
|
|
7828
8582
|
}
|
|
7829
8583
|
console.error(errorMessage(presign.body, `上传失败:${filename}`));
|
|
@@ -7831,16 +8585,21 @@ async function storageUploadOne(credentials, filePath) {
|
|
|
7831
8585
|
}
|
|
7832
8586
|
async function storageUpload(paths) {
|
|
7833
8587
|
const files = [];
|
|
8588
|
+
let encrypt = false;
|
|
7834
8589
|
for (let i = 0;i < paths.length; i++) {
|
|
7835
8590
|
if (paths[i].startsWith("--")) {
|
|
7836
|
-
if (paths[i] === "--passphrase")
|
|
8591
|
+
if (paths[i] === "--passphrase") {
|
|
8592
|
+
encrypt = true;
|
|
7837
8593
|
i++;
|
|
8594
|
+
}
|
|
7838
8595
|
continue;
|
|
7839
8596
|
}
|
|
7840
8597
|
files.push(paths[i]);
|
|
7841
8598
|
}
|
|
8599
|
+
if (!encrypt && process.env.CRAB_PASSPHRASE)
|
|
8600
|
+
encrypt = true;
|
|
7842
8601
|
if (files.length === 0) {
|
|
7843
|
-
console.error("用法:crab storage upload <file...> [--passphrase 口令] [--json]");
|
|
8602
|
+
console.error("用法:crab storage upload <file...> [--passphrase 口令] [--json](缺省明文上传;带口令则端到端加密)");
|
|
7844
8603
|
return EXIT_USAGE;
|
|
7845
8604
|
}
|
|
7846
8605
|
const credentials = requireCredentials();
|
|
@@ -7848,7 +8607,7 @@ async function storageUpload(paths) {
|
|
|
7848
8607
|
return credentials;
|
|
7849
8608
|
let code = 0;
|
|
7850
8609
|
for (const file of files) {
|
|
7851
|
-
const result = await storageUploadOne(credentials, file);
|
|
8610
|
+
const result = await storageUploadOne(credentials, file, encrypt);
|
|
7852
8611
|
if (result !== 0 && code === 0)
|
|
7853
8612
|
code = result;
|
|
7854
8613
|
if (result === EXIT_AUTH || result === EXIT_SCOPE)
|
|
@@ -8144,9 +8903,10 @@ async function storageQuota() {
|
|
|
8144
8903
|
return 0;
|
|
8145
8904
|
}
|
|
8146
8905
|
const pct = quota.quotaBytes > 0 ? (quota.usedBytes / quota.quotaBytes * 100).toFixed(1) : "0";
|
|
8906
|
+
const libraryCount = (quota.counts.all ?? 0) - (quota.counts.vault ?? 0);
|
|
8147
8907
|
console.log(`已用 ${formatBytesCli(quota.usedBytes)} / ${formatBytesCli(quota.quotaBytes)}(${pct}%)`);
|
|
8148
|
-
console.log(` 文件 ${formatBytesCli(quota.bySource.files)} · 素材 ${formatBytesCli(quota.bySource.media)} ·
|
|
8149
|
-
console.log(` 对象数:全部 ${quota.counts.all ?? 0} ·
|
|
8908
|
+
console.log(` 文件 ${formatBytesCli(quota.bySource.files)} · 素材 ${formatBytesCli(quota.bySource.media)} · ` + `邮件附件 ${formatBytesCli(quota.bySource.mail)} · 协作 ${formatBytesCli(quota.bySource.collab)} · ` + `Agent ${formatBytesCli(quota.bySource.agent)} · 回收站 ${formatBytesCli(quota.trashBytes)}`);
|
|
8909
|
+
console.log(` 对象数:全部 ${quota.counts.all ?? 0} · 素材库 ${libraryCount}(保险箱 ${quota.counts.vault ?? 0}) · ` + `共享 ${quota.counts.shared ?? 0} · 回收站 ${quota.counts.trash ?? 0}`);
|
|
8150
8910
|
return 0;
|
|
8151
8911
|
}
|
|
8152
8912
|
async function storageCategories() {
|
|
@@ -8407,7 +9167,7 @@ async function collabSpacesList() {
|
|
|
8407
9167
|
}
|
|
8408
9168
|
for (const s of data.spaces) {
|
|
8409
9169
|
const storage = s.kind === "independent" ? ` · 存储 ${formatBytesCli(s.storageUsedBytes)}/${formatBytesCli(s.storageAllocatedBytes)}` : "";
|
|
8410
|
-
console.log(`${collabSpaceToken(s)} ${s.status === "archived" ? "[已归档·只读] " : ""}任务 ${s.taskCount}/${s.limits.taskLimit} ·
|
|
9170
|
+
console.log(`${collabSpaceToken(s)} ${s.status === "archived" ? "[已归档·只读] " : ""}任务 ${s.taskCount}/${s.limits.taskLimit} · 成员 ${s.memberCount}/${s.limits.memberLimit}${storage}`);
|
|
8411
9171
|
console.log(` id ${s.id}`);
|
|
8412
9172
|
}
|
|
8413
9173
|
console.log(`独立空间名额:${data.independentUsed}/${data.independentLimit}(创建消耗 5 积分:crab collab space create <名称>)`);
|
|
@@ -8846,131 +9606,1798 @@ async function logout() {
|
|
|
8846
9606
|
console.log("✓ 已撤销 Agent Token 并清除本地凭证。");
|
|
8847
9607
|
return 0;
|
|
8848
9608
|
}
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
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);
|
|
8867
9654
|
}
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
if (sub === "list")
|
|
8873
|
-
process.exit(listSkills());
|
|
8874
|
-
if (sub === "uninstall")
|
|
8875
|
-
process.exit(uninstallSkills(args.slice(2)));
|
|
8876
|
-
console.error(`用法:crab skills install | list | uninstall [--agent claude,zcode,codex,agents]
|
|
8877
|
-
`);
|
|
8878
|
-
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;
|
|
8879
9659
|
}
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
if (
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
if (sub === "search")
|
|
8889
|
-
process.exit(await mailSearch(args.slice(2).join(" ") || undefined));
|
|
8890
|
-
if (sub === "send")
|
|
8891
|
-
process.exit(await mailSend());
|
|
8892
|
-
if (sub === "archive" || sub === "trash" || sub === "delete")
|
|
8893
|
-
process.exit(await mailMove(sub, args[2]));
|
|
8894
|
-
if (sub === "folders")
|
|
8895
|
-
process.exit(await mailFoldersOverview());
|
|
8896
|
-
if (sub === "activity")
|
|
8897
|
-
process.exit(await mailActivity());
|
|
8898
|
-
console.error(sub ? `未知子命令:mail ${sub}
|
|
8899
|
-
` : `缺少子命令:mail list | read | unread | search | send | archive | trash | delete | folders | activity
|
|
8900
|
-
`);
|
|
8901
|
-
console.log(usage);
|
|
8902
|
-
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;
|
|
8903
9668
|
}
|
|
8904
|
-
|
|
8905
|
-
const
|
|
8906
|
-
if (
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
process.exit(await contactsInvitees());
|
|
8916
|
-
console.error(sub ? `未知子命令:contacts ${sub}
|
|
8917
|
-
` : `缺少子命令:contacts list | add | rm | invite | invitees
|
|
8918
|
-
`);
|
|
8919
|
-
console.log(usage);
|
|
8920
|
-
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;
|
|
8921
9680
|
}
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
process.exit(await storageMkCat(args.slice(2)));
|
|
8948
|
-
if (sub === "rencat")
|
|
8949
|
-
process.exit(await storageRenCat(args[2], args.slice(3)));
|
|
8950
|
-
if (sub === "rmcat")
|
|
8951
|
-
process.exit(await storageRmCat(args[2]));
|
|
8952
|
-
if (sub === "tags")
|
|
8953
|
-
process.exit(await storageTags());
|
|
8954
|
-
if (sub === "share")
|
|
8955
|
-
process.exit(await storageShareCmd(args[2]));
|
|
8956
|
-
if (sub === "shares")
|
|
8957
|
-
process.exit(await storageShares(args[2]));
|
|
8958
|
-
if (sub === "unshare")
|
|
8959
|
-
process.exit(await storageUnshare(args[2]));
|
|
8960
|
-
if (sub === "key")
|
|
8961
|
-
process.exit(await storageKeyCmd());
|
|
8962
|
-
if (sub === "activity")
|
|
8963
|
-
process.exit(await storageActivity());
|
|
8964
|
-
console.error(sub ? `未知子命令:storage ${sub}
|
|
8965
|
-
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | rencat | rmcat | tags | share | shares | unshare | key | activity
|
|
8966
|
-
`);
|
|
8967
|
-
console.log(usage);
|
|
8968
|
-
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);
|
|
8969
9706
|
}
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
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;
|
|
9712
|
+
}
|
|
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;
|
|
9728
|
+
}
|
|
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());
|
|
8974
11401
|
if (sub === "history")
|
|
8975
11402
|
process.exit(await creditsHistory());
|
|
8976
11403
|
if (sub === "pricing")
|
|
@@ -9019,6 +11446,76 @@ switch (command) {
|
|
|
9019
11446
|
}
|
|
9020
11447
|
case "post":
|
|
9021
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);
|
|
11518
|
+
}
|
|
9022
11519
|
case "logout":
|
|
9023
11520
|
process.exit(await logout());
|
|
9024
11521
|
case "version":
|
|
@@ -9037,3 +11534,73 @@ switch (command) {
|
|
|
9037
11534
|
console.log(usage);
|
|
9038
11535
|
process.exit(EXIT_USAGE);
|
|
9039
11536
|
}
|
|
11537
|
+
async function agentStatus() {
|
|
11538
|
+
const credentials = requireCredentials();
|
|
11539
|
+
if (typeof credentials === "number")
|
|
11540
|
+
return credentials;
|
|
11541
|
+
const [sessionsRes, approvalsRes] = await Promise.all([
|
|
11542
|
+
api2(credentials.apiBase, "/v1/agent/sessions", { headers: authHeaders(credentials) }),
|
|
11543
|
+
api2(credentials.apiBase, "/v1/agent/approvals", { headers: authHeaders(credentials) })
|
|
11544
|
+
]);
|
|
11545
|
+
if (sessionsRes.status !== 200 || approvalsRes.status !== 200) {
|
|
11546
|
+
console.error(errorMessage(sessionsRes.body ?? approvalsRes.body, "获取 Agent 状态失败。"));
|
|
11547
|
+
if (sessionsRes.body?.error?.code === "scope_denied") {
|
|
11548
|
+
console.error("提示:需要 agent.read scope——运行 crab login --scopes agent 重新授权。");
|
|
11549
|
+
}
|
|
11550
|
+
return 1;
|
|
11551
|
+
}
|
|
11552
|
+
const sessions = sessionsRes.body.data?.sessions ?? [];
|
|
11553
|
+
const approvals = approvalsRes.body.data?.approvals ?? [];
|
|
11554
|
+
const running = sessions.filter((x) => x.status === "running").length;
|
|
11555
|
+
const pending = approvals.filter((x) => x.status === "pending").length;
|
|
11556
|
+
if (hasFlag("--json")) {
|
|
11557
|
+
console.log(JSON.stringify({ sessions: sessions.length, running, pendingApprovals: pending }, null, 2));
|
|
11558
|
+
return 0;
|
|
11559
|
+
}
|
|
11560
|
+
console.log(`个人云 Agent:会话 ${sessions.length} · 运行中 ${running} · 待审批 ${pending}`);
|
|
11561
|
+
return 0;
|
|
11562
|
+
}
|
|
11563
|
+
async function agentSessions() {
|
|
11564
|
+
const credentials = requireCredentials();
|
|
11565
|
+
if (typeof credentials === "number")
|
|
11566
|
+
return credentials;
|
|
11567
|
+
const res = await api2(credentials.apiBase, "/v1/agent/sessions", { headers: authHeaders(credentials) });
|
|
11568
|
+
if (res.status !== 200) {
|
|
11569
|
+
console.error(errorMessage(res.body, "获取会话列表失败。"));
|
|
11570
|
+
return 1;
|
|
11571
|
+
}
|
|
11572
|
+
const sessions = res.body.data?.sessions ?? [];
|
|
11573
|
+
if (hasFlag("--json")) {
|
|
11574
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
11575
|
+
return 0;
|
|
11576
|
+
}
|
|
11577
|
+
for (const x of sessions) {
|
|
11578
|
+
console.log(`${x.kind === "task" ? "[任务]" : "[对话]"} ${x.title} ${x.status}${x.repeatCron ? ` cron ${x.repeatCron}` : ""} ${new Date(x.updatedAt).toLocaleString("zh-CN")}`);
|
|
11579
|
+
}
|
|
11580
|
+
console.log(`共 ${sessions.length} 个会话`);
|
|
11581
|
+
return 0;
|
|
11582
|
+
}
|
|
11583
|
+
async function agentApprovals() {
|
|
11584
|
+
const credentials = requireCredentials();
|
|
11585
|
+
if (typeof credentials === "number")
|
|
11586
|
+
return credentials;
|
|
11587
|
+
const res = await api2(credentials.apiBase, "/v1/agent/approvals", { headers: authHeaders(credentials) });
|
|
11588
|
+
if (res.status !== 200) {
|
|
11589
|
+
console.error(errorMessage(res.body, "获取审批队列失败。"));
|
|
11590
|
+
return 1;
|
|
11591
|
+
}
|
|
11592
|
+
const approvals = res.body.data?.approvals ?? [];
|
|
11593
|
+
if (hasFlag("--json")) {
|
|
11594
|
+
console.log(JSON.stringify(approvals, null, 2));
|
|
11595
|
+
return 0;
|
|
11596
|
+
}
|
|
11597
|
+
if (approvals.length === 0) {
|
|
11598
|
+
console.log("没有待处理或已过期的审批。");
|
|
11599
|
+
return 0;
|
|
11600
|
+
}
|
|
11601
|
+
for (const x of approvals) {
|
|
11602
|
+
console.log(`${x.status === "pending" ? "[待审批]" : "[已超时]"} ${x.tool} ${x.sessionTitle}`);
|
|
11603
|
+
console.log(` ${x.summary.slice(0, 160)}`);
|
|
11604
|
+
}
|
|
11605
|
+
return 0;
|
|
11606
|
+
}
|