@crabcloud/cli 0.2.0 → 0.2.2
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
CHANGED
|
@@ -16,7 +16,9 @@ var __export = (target, all) => {
|
|
|
16
16
|
|
|
17
17
|
// src/index.ts
|
|
18
18
|
import { hostname, homedir as homedir2 } from "node:os";
|
|
19
|
-
import {
|
|
19
|
+
import { createInterface } from "node:readline";
|
|
20
|
+
import { Writable } from "node:stream";
|
|
21
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
20
22
|
import { basename, join as join2 } from "node:path";
|
|
21
23
|
|
|
22
24
|
// ../../node_modules/.bun/zod@4.6.5/node_modules/zod/v4/core/util.js
|
|
@@ -5897,8 +5899,8 @@ function date2(params) {
|
|
|
5897
5899
|
var accountDomain = "crabcloud.cc";
|
|
5898
5900
|
var adminDomain = `admin.${accountDomain}`;
|
|
5899
5901
|
var storageDomain = `storage.${accountDomain}`;
|
|
5900
|
-
function storagePublicUrl(objectId) {
|
|
5901
|
-
return `https://${storageDomain}/o/${objectId}`;
|
|
5902
|
+
function storagePublicUrl(objectId, originOverride) {
|
|
5903
|
+
return `${originOverride ?? `https://${storageDomain}`}/o/${objectId}`;
|
|
5902
5904
|
}
|
|
5903
5905
|
var clientTypes = ["web", "agent", "miniapp", "native"];
|
|
5904
5906
|
var scopes = [
|
|
@@ -6192,7 +6194,10 @@ var collabAllocationInput = object({
|
|
|
6192
6194
|
});
|
|
6193
6195
|
var collabBoostInput = object({ kind: _enum(collabBoostKinds) });
|
|
6194
6196
|
var collabInviteInput = object({ username: usernameSchema });
|
|
6195
|
-
var
|
|
6197
|
+
var collabInviteAcceptInput = object({
|
|
6198
|
+
token: string2().trim().min(1).max(64).transform((v) => v.toUpperCase())
|
|
6199
|
+
});
|
|
6200
|
+
var collabLinkInput = string2().trim().url().max(2000).regex(/^https?:\/\//i);
|
|
6196
6201
|
var collabCreateTaskInput = object({
|
|
6197
6202
|
title: string2().trim().min(1).max(COLLAB_TITLE_MAX),
|
|
6198
6203
|
body: string2().max(COLLAB_RECORD_BODY_MAX).default(""),
|
|
@@ -6451,6 +6456,7 @@ function stripCodexSection(content) {
|
|
|
6451
6456
|
var DEFAULT_API = "https://api.crabcloud.cc";
|
|
6452
6457
|
var DEFAULT_SCOPES = ["account.read"];
|
|
6453
6458
|
var CONFIG_PATH = process.env.CRAB_CREDENTIALS ?? join2(homedir2(), ".config", "crabcloud", "credentials.json");
|
|
6459
|
+
var CONFIG_DIR = join2(homedir2(), ".config", "crabcloud");
|
|
6454
6460
|
var EXIT_USAGE = 2;
|
|
6455
6461
|
var EXIT_AUTH = 3;
|
|
6456
6462
|
var EXIT_SCOPE = 4;
|
|
@@ -6540,7 +6546,10 @@ function formatTime(ms) {
|
|
|
6540
6546
|
return new Date(ms).toLocaleString("sv", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
6541
6547
|
}
|
|
6542
6548
|
function fmtCredits(cents) {
|
|
6543
|
-
return cents.toLocaleString("en-US");
|
|
6549
|
+
return (cents / 100).toLocaleString("en-US");
|
|
6550
|
+
}
|
|
6551
|
+
function fmtUsd(cents) {
|
|
6552
|
+
return `$${cents / 100}`;
|
|
6544
6553
|
}
|
|
6545
6554
|
var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
6546
6555
|
发布物经 npm 分发:npx @crabcloud/cli <command>(全局安装后短名 crab 等价)
|
|
@@ -6586,14 +6595,17 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6586
6595
|
[--type doc|image|video|archive|code] [--tag a,b] [--trash]
|
|
6587
6596
|
[--shared] [--recent] [--message-id <id>] [--limit N] [--offset N] [--json]
|
|
6588
6597
|
列出云盘对象(跨来源统一视图,能力与 Web 云盘一致)
|
|
6589
|
-
storage upload <file...> [--json]
|
|
6598
|
+
storage upload <file...> [--json] 上传一个或多个文件(直传优先,自动回退中转);
|
|
6599
|
+
files 域默认端到端加密,口令 --passphrase /
|
|
6600
|
+
CRAB_PASSPHRASE 提供,终端内运行时交互输入
|
|
6590
6601
|
storage download <id|文件名> [-o 输出路径]
|
|
6591
|
-
|
|
6602
|
+
下载对象内容(默认按原文件名存到当前目录;
|
|
6603
|
+
加密对象需口令,终端内运行时交互输入)
|
|
6592
6604
|
storage rename <id|文件名> <新名称> 重命名
|
|
6593
6605
|
storage tag <id|文件名> --add a,b [--remove c,d]
|
|
6594
6606
|
增删标签
|
|
6595
|
-
storage category <id|文件名>
|
|
6596
|
-
|
|
6607
|
+
storage category <id|文件名> <分类id|分类名|none>
|
|
6608
|
+
设置分类(分类见 storage categories)
|
|
6597
6609
|
storage publish|unpublish <id|文件名>
|
|
6598
6610
|
公开 / 收回公开(公开时打印公开直链)
|
|
6599
6611
|
storage rm|restore|purge <id|文件名>
|
|
@@ -6601,11 +6613,17 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6601
6613
|
storage quota [--json] 存储水位(统一容量账:文件/素材/邮件附件一本账)
|
|
6602
6614
|
storage share <id|文件名> [--expires 7|30|90|forever] [--passcode 提取码]
|
|
6603
6615
|
创建分享链接(加密文件链接自带解密钥匙 #fragment)
|
|
6616
|
+
storage key [--remember|--forget]
|
|
6617
|
+
本机记住的云盘钥匙管理:默认查看状态;--remember 解锁一次
|
|
6618
|
+
并记住(下次免输口令);--forget 清除
|
|
6604
6619
|
storage shares <id|文件名> [--json]
|
|
6605
6620
|
该文件的分享链接列表(状态/下载计数)
|
|
6606
6621
|
storage unshare <shareId> 撤销分享
|
|
6607
6622
|
storage categories [--json] 分类列表
|
|
6608
|
-
storage mkcat <名称>
|
|
6623
|
+
storage mkcat <名称> [--color 色] 新建分类(颜色缺省 action)
|
|
6624
|
+
storage rencat <id|分类名> <新名称> [--color 色]
|
|
6625
|
+
分类重命名 / 改色
|
|
6626
|
+
storage rmcat <id|分类名> 删除分类(其中文件回落未分类)
|
|
6609
6627
|
storage tags [--json] 标签汇总
|
|
6610
6628
|
collab spaces [--json] 列出我所在的协作空间(任务/成员计数与限额)
|
|
6611
6629
|
collab space create <名称> [--allocation-mb N] [--json]
|
|
@@ -6821,7 +6839,7 @@ async function creditsHistory() {
|
|
|
6821
6839
|
console.log(`${formatTime(entry.createdAt)} ${CREDIT_REASON_LABELS[entry.reason] ?? entry.reason} ${signed}`);
|
|
6822
6840
|
}
|
|
6823
6841
|
console.log(`
|
|
6824
|
-
共 ${
|
|
6842
|
+
共 ${data.total.toLocaleString("en-US")} 条(本页 ${data.ledger.length} 条,--offset 翻页)`);
|
|
6825
6843
|
return 0;
|
|
6826
6844
|
}
|
|
6827
6845
|
async function creditsPricing() {
|
|
@@ -6841,10 +6859,10 @@ async function creditsPricing() {
|
|
|
6841
6859
|
console.log("暂无动作计价项。");
|
|
6842
6860
|
return 0;
|
|
6843
6861
|
}
|
|
6844
|
-
console.log("动作计价(
|
|
6862
|
+
console.log("动作计价(1 积分 = $1,价目为 $ 口径):");
|
|
6845
6863
|
for (const item of actions) {
|
|
6846
6864
|
const label = CREDIT_ACTION_LABELS[item.action] ?? item.action;
|
|
6847
|
-
console.log(` ${item.action.padEnd(24)} ${label} ${
|
|
6865
|
+
console.log(` ${item.action.padEnd(24)} ${label} ${fmtUsd(item.creditCents)}`);
|
|
6848
6866
|
}
|
|
6849
6867
|
return 0;
|
|
6850
6868
|
}
|
|
@@ -7122,7 +7140,7 @@ async function mailSend() {
|
|
|
7122
7140
|
if (attachmentIds.length > 0)
|
|
7123
7141
|
console.log(` 附件 ${attachmentIds.length} 个(云盘引用,不复制)`);
|
|
7124
7142
|
if (result.chargedCredits > 0)
|
|
7125
|
-
console.log(`
|
|
7143
|
+
console.log(` 消耗:${fmtUsd(result.chargedCredits)}`);
|
|
7126
7144
|
if (result.failedExternal > 0)
|
|
7127
7145
|
console.log(` ⚠ ${result.failedExternal} 个外部收件人发送失败,对应积分已退回`);
|
|
7128
7146
|
return 0;
|
|
@@ -7444,13 +7462,168 @@ var utf82 = (s) => new TextEncoder().encode(s);
|
|
|
7444
7462
|
var MASTER_AAD = utf82("drive-master");
|
|
7445
7463
|
var DATA_AAD = utf82("drive-data");
|
|
7446
7464
|
var KEK_ITERATIONS = 210000;
|
|
7447
|
-
|
|
7448
|
-
|
|
7465
|
+
var passphraseMemo;
|
|
7466
|
+
async function drivePassphrase() {
|
|
7467
|
+
if (passphraseMemo !== undefined)
|
|
7468
|
+
return passphraseMemo;
|
|
7469
|
+
const explicit = readFlag("--passphrase") ?? process.env.CRAB_PASSPHRASE;
|
|
7470
|
+
if (explicit !== undefined && explicit !== "") {
|
|
7471
|
+
passphraseMemo = explicit;
|
|
7472
|
+
return explicit;
|
|
7473
|
+
}
|
|
7474
|
+
if (!process.stdin.isTTY)
|
|
7475
|
+
return;
|
|
7476
|
+
process.stderr.write("请输入云盘口令(输入不回显)Drive passphrase: ");
|
|
7477
|
+
const typed = await askHidden();
|
|
7478
|
+
if (typed.length === 0)
|
|
7479
|
+
return;
|
|
7480
|
+
passphraseMemo = typed;
|
|
7481
|
+
return typed;
|
|
7482
|
+
}
|
|
7483
|
+
function askHidden() {
|
|
7484
|
+
return new Promise((resolve) => {
|
|
7485
|
+
const silent = new Writable({ write(_chunk, _enc, cb) {
|
|
7486
|
+
cb();
|
|
7487
|
+
} });
|
|
7488
|
+
const rl = createInterface({ input: process.stdin, output: silent, terminal: true });
|
|
7489
|
+
rl.question("", (answer) => {
|
|
7490
|
+
rl.close();
|
|
7491
|
+
process.stderr.write(`
|
|
7492
|
+
`);
|
|
7493
|
+
resolve(answer);
|
|
7494
|
+
});
|
|
7495
|
+
});
|
|
7449
7496
|
}
|
|
7450
|
-
|
|
7451
|
-
|
|
7497
|
+
function askVisible(question) {
|
|
7498
|
+
return new Promise((resolve) => {
|
|
7499
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
7500
|
+
rl.question(question, (answer) => {
|
|
7501
|
+
rl.close();
|
|
7502
|
+
resolve(answer);
|
|
7503
|
+
});
|
|
7504
|
+
});
|
|
7505
|
+
}
|
|
7506
|
+
var CANARY_PLAIN = utf82("crab-drive-verify");
|
|
7507
|
+
var CANARY_AAD = utf82("drive-verify");
|
|
7508
|
+
function driveKeyPath(accountId) {
|
|
7509
|
+
return join2(CONFIG_DIR, `drive-key-${accountId}.json`);
|
|
7510
|
+
}
|
|
7511
|
+
function readDriveKeyFile(accountId) {
|
|
7512
|
+
try {
|
|
7513
|
+
return JSON.parse(readFileSync2(driveKeyPath(accountId), "utf8"));
|
|
7514
|
+
} catch {
|
|
7515
|
+
return null;
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
async function loadVerifiedDriveKey(credentials, accountId) {
|
|
7519
|
+
const file = readDriveKeyFile(accountId);
|
|
7520
|
+
if (!file || file.apiBase !== credentials.apiBase || typeof file.masterKey !== "string" || typeof file.canary !== "string") {
|
|
7521
|
+
return null;
|
|
7522
|
+
}
|
|
7523
|
+
try {
|
|
7524
|
+
const key = await importAesKey(b64urlDecode(file.masterKey));
|
|
7525
|
+
await aeadDecrypt(key, sealFromB64url(file.canary), CANARY_AAD);
|
|
7526
|
+
return b64urlDecode(file.masterKey);
|
|
7527
|
+
} catch {
|
|
7528
|
+
forgetDriveKey(accountId);
|
|
7529
|
+
console.error("本机记住的云盘钥匙已失效(可能在别处轮换),请改用口令。");
|
|
7530
|
+
return null;
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
async function storeDriveKey(accountId, apiBase, masterRaw) {
|
|
7534
|
+
const sealed = await aeadEncrypt(await importAesKey(masterRaw), CANARY_PLAIN, CANARY_AAD);
|
|
7535
|
+
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
7536
|
+
writeFileSync2(driveKeyPath(accountId), `${JSON.stringify({ apiBase, masterKey: b64urlEncode(masterRaw), canary: sealToB64url({ iv: sealed.iv, ciphertext: sealed.ciphertext }), storedAt: Date.now() }, null, 2)}
|
|
7537
|
+
`, { mode: 384 });
|
|
7538
|
+
}
|
|
7539
|
+
function forgetDriveKey(accountId) {
|
|
7540
|
+
rmSync2(driveKeyPath(accountId), { force: true });
|
|
7541
|
+
}
|
|
7542
|
+
var accountIdMemo;
|
|
7543
|
+
async function driveAccountId(credentials) {
|
|
7544
|
+
if (accountIdMemo !== undefined)
|
|
7545
|
+
return accountIdMemo;
|
|
7546
|
+
const res = await api2(credentials.apiBase, "/v1/auth/me", { headers: authHeaders(credentials) });
|
|
7547
|
+
accountIdMemo = res.status === 200 ? res.body.data.account.id : null;
|
|
7548
|
+
return accountIdMemo;
|
|
7549
|
+
}
|
|
7550
|
+
async function maybeRememberDriveKey(credentials, masterRaw) {
|
|
7551
|
+
if (!process.stdin.isTTY)
|
|
7552
|
+
return;
|
|
7553
|
+
const accountId = await driveAccountId(credentials);
|
|
7554
|
+
const existing = accountId ? readDriveKeyFile(accountId) : null;
|
|
7555
|
+
if (accountId && existing && existing.apiBase === credentials.apiBase && existing.masterKey)
|
|
7556
|
+
return;
|
|
7557
|
+
const answer = await askVisible("记住云盘钥匙到本机?下次免输口令(storage key --forget 清除)[y/N] ");
|
|
7558
|
+
if (answer.trim().toLowerCase() === "y") {
|
|
7559
|
+
await storeDriveKey(accountId, credentials.apiBase, masterRaw);
|
|
7560
|
+
console.log("✓ 已记住云盘钥匙到本机(storage key --forget 可清除)。");
|
|
7561
|
+
}
|
|
7562
|
+
}
|
|
7563
|
+
async function storageKeyCmd() {
|
|
7564
|
+
const credentials = requireCredentials();
|
|
7565
|
+
if (typeof credentials === "number")
|
|
7566
|
+
return credentials;
|
|
7567
|
+
const accountId = await driveAccountId(credentials);
|
|
7568
|
+
if (!accountId) {
|
|
7569
|
+
console.error("获取账号身份失败(凭证无效?)。");
|
|
7570
|
+
return EXIT_AUTH;
|
|
7571
|
+
}
|
|
7572
|
+
if (hasFlag("--forget")) {
|
|
7573
|
+
const had = readDriveKeyFile(accountId)?.masterKey !== undefined;
|
|
7574
|
+
forgetDriveKey(accountId);
|
|
7575
|
+
console.log(had ? "✓ 已清除本机记住的云盘钥匙,下次操作恢复口令输入。" : "本机没有记住的云盘钥匙。");
|
|
7576
|
+
return 0;
|
|
7577
|
+
}
|
|
7578
|
+
if (hasFlag("--remember")) {
|
|
7579
|
+
const existing = readDriveKeyFile(accountId);
|
|
7580
|
+
if (existing && existing.apiBase === credentials.apiBase && existing.masterKey) {
|
|
7581
|
+
console.log(`本机已记住云盘钥匙:${driveKeyPath(accountId)}`);
|
|
7582
|
+
return 0;
|
|
7583
|
+
}
|
|
7584
|
+
const raw = await driveMasterKeyRaw(credentials, { skipRememberPrompt: true });
|
|
7585
|
+
if (typeof raw === "number")
|
|
7586
|
+
return raw;
|
|
7587
|
+
await storeDriveKey(accountId, credentials.apiBase, raw);
|
|
7588
|
+
console.log(`✓ 已记住云盘钥匙到本机:${driveKeyPath(accountId)}(storage key --forget 清除)`);
|
|
7589
|
+
return 0;
|
|
7590
|
+
}
|
|
7591
|
+
const file = readDriveKeyFile(accountId);
|
|
7592
|
+
if (file && file.apiBase === credentials.apiBase && file.masterKey) {
|
|
7593
|
+
const when = typeof file.storedAt === "number" ? new Date(file.storedAt).toLocaleString() : "未知时间";
|
|
7594
|
+
console.log(`✓ 本机已记住云盘钥匙:${driveKeyPath(accountId)}(记住于 ${when})`);
|
|
7595
|
+
console.log(" 之后 storage 命令免输口令;Web 端轮换口令/恢复码后本钥匙自动失效。");
|
|
7596
|
+
console.log(" storage key --forget 清除;storage key --remember 重新解锁并记住。");
|
|
7597
|
+
} else {
|
|
7598
|
+
console.log("本机未记住云盘钥匙(每次操作需输入口令)。");
|
|
7599
|
+
console.log(" storage key --remember:解锁一次并记住;storage key --forget:清除记录。");
|
|
7600
|
+
}
|
|
7601
|
+
return 0;
|
|
7602
|
+
}
|
|
7603
|
+
var masterKeyRawMemo = null;
|
|
7604
|
+
async function driveMasterKeyRaw(credentials, opts = {}) {
|
|
7605
|
+
if (masterKeyRawMemo)
|
|
7606
|
+
return masterKeyRawMemo;
|
|
7607
|
+
const accountId = await driveAccountId(credentials);
|
|
7608
|
+
if (accountId) {
|
|
7609
|
+
const stored = await loadVerifiedDriveKey(credentials, accountId);
|
|
7610
|
+
if (stored) {
|
|
7611
|
+
const res = await api2(credentials.apiBase, "/v1/storage/keys", { headers: authHeaders(credentials) });
|
|
7612
|
+
if (res.status === 200) {
|
|
7613
|
+
masterKeyRawMemo = stored;
|
|
7614
|
+
return stored;
|
|
7615
|
+
}
|
|
7616
|
+
if (res.status === 404) {
|
|
7617
|
+
forgetDriveKey(accountId);
|
|
7618
|
+
} else {
|
|
7619
|
+
console.error(errorMessage(res.body, "获取云盘密钥失败。"));
|
|
7620
|
+
return exitCodeForApiError(res.body);
|
|
7621
|
+
}
|
|
7622
|
+
}
|
|
7623
|
+
}
|
|
7624
|
+
const passphrase = await drivePassphrase();
|
|
7452
7625
|
if (!passphrase) {
|
|
7453
|
-
console.error("需要云盘口令:--passphrase <口令> 或环境变量 CRAB_PASSPHRASE
|
|
7626
|
+
console.error("需要云盘口令:--passphrase <口令> 或环境变量 CRAB_PASSPHRASE(交互终端会提示输入)。");
|
|
7454
7627
|
return EXIT_USAGE;
|
|
7455
7628
|
}
|
|
7456
7629
|
const res = await api2(credentials.apiBase, "/v1/storage/keys", { headers: authHeaders(credentials) });
|
|
@@ -7479,7 +7652,10 @@ async function driveMasterKey(credentials) {
|
|
|
7479
7652
|
}
|
|
7480
7653
|
console.log(`★ 恢复码:${recoveryCode}`);
|
|
7481
7654
|
console.log(" (口令遗忘时的唯一找回方式;平台无后门,丢失即数据不可解)");
|
|
7482
|
-
|
|
7655
|
+
if (!opts.skipRememberPrompt)
|
|
7656
|
+
await maybeRememberDriveKey(credentials, masterRaw);
|
|
7657
|
+
masterKeyRawMemo = masterRaw;
|
|
7658
|
+
return masterRaw;
|
|
7483
7659
|
}
|
|
7484
7660
|
if (res.status !== 200) {
|
|
7485
7661
|
console.error(errorMessage(res.body, "获取云盘密钥失败。"));
|
|
@@ -7488,12 +7664,22 @@ async function driveMasterKey(credentials) {
|
|
|
7488
7664
|
const keys = res.body.data;
|
|
7489
7665
|
try {
|
|
7490
7666
|
const kek = await importAesKey(await deriveKekBits(passphrase, b64urlDecode(keys.kdfSalt), keys.kdfIterations));
|
|
7491
|
-
|
|
7667
|
+
const masterRaw = await unwrapKeyBytes(kek, sealFromB64url(keys.wrappedMasterKey.slice(3)), MASTER_AAD);
|
|
7668
|
+
if (!opts.skipRememberPrompt)
|
|
7669
|
+
await maybeRememberDriveKey(credentials, masterRaw);
|
|
7670
|
+
masterKeyRawMemo = masterRaw;
|
|
7671
|
+
return masterRaw;
|
|
7492
7672
|
} catch {
|
|
7493
7673
|
console.error("口令错误。");
|
|
7494
7674
|
return 1;
|
|
7495
7675
|
}
|
|
7496
7676
|
}
|
|
7677
|
+
async function driveMasterKey(credentials) {
|
|
7678
|
+
const raw = await driveMasterKeyRaw(credentials);
|
|
7679
|
+
if (typeof raw === "number")
|
|
7680
|
+
return raw;
|
|
7681
|
+
return importAesKey(raw);
|
|
7682
|
+
}
|
|
7497
7683
|
async function storageUploadOne(credentials, filePath) {
|
|
7498
7684
|
let buf;
|
|
7499
7685
|
try {
|
|
@@ -7637,11 +7823,6 @@ async function storageDownload(token) {
|
|
|
7637
7823
|
let bytes = new Uint8Array(await res.arrayBuffer());
|
|
7638
7824
|
const wrapped = res.headers.get("x-drive-wrapped-key");
|
|
7639
7825
|
if (wrapped?.startsWith("u1.")) {
|
|
7640
|
-
const passphrase = drivePassphrase();
|
|
7641
|
-
if (!passphrase) {
|
|
7642
|
-
console.error("该文件已加密:--passphrase <口令> 或 CRAB_PASSPHRASE 提供云盘口令后重试。");
|
|
7643
|
-
return EXIT_USAGE;
|
|
7644
|
-
}
|
|
7645
7826
|
const master = await driveMasterKey(credentials);
|
|
7646
7827
|
if (typeof master === "number")
|
|
7647
7828
|
return master;
|
|
@@ -7708,13 +7889,20 @@ async function storageTag(token) {
|
|
|
7708
7889
|
}
|
|
7709
7890
|
async function storageCategory(token, value) {
|
|
7710
7891
|
if (!token || !value) {
|
|
7711
|
-
console.error("用法:crab storage category <id|文件名>
|
|
7892
|
+
console.error("用法:crab storage category <id|文件名> <分类id|分类名|none>(分类见 storage categories)");
|
|
7712
7893
|
return EXIT_USAGE;
|
|
7713
7894
|
}
|
|
7714
7895
|
const credentials = requireCredentials();
|
|
7715
7896
|
if (typeof credentials === "number")
|
|
7716
7897
|
return credentials;
|
|
7717
|
-
|
|
7898
|
+
let categoryId = null;
|
|
7899
|
+
if (value !== "none") {
|
|
7900
|
+
const resolved = await resolveCategoryId(credentials, value);
|
|
7901
|
+
if (typeof resolved === "number")
|
|
7902
|
+
return resolved;
|
|
7903
|
+
categoryId = resolved;
|
|
7904
|
+
}
|
|
7905
|
+
const result = await storagePatch(credentials, token, { categoryId });
|
|
7718
7906
|
if (typeof result === "number")
|
|
7719
7907
|
return result;
|
|
7720
7908
|
console.log(`✓ 分类已更新:${result.filename}`);
|
|
@@ -7732,8 +7920,10 @@ async function storagePublish(token, publish) {
|
|
|
7732
7920
|
if (typeof result === "number")
|
|
7733
7921
|
return result;
|
|
7734
7922
|
console.log(`✓ ${publish ? "已公开" : "已收回公开"}:${result.filename}`);
|
|
7735
|
-
if (publish)
|
|
7736
|
-
|
|
7923
|
+
if (publish) {
|
|
7924
|
+
const base = credentials.apiBase === DEFAULT_API ? undefined : credentials.apiBase;
|
|
7925
|
+
console.log(` 公开链接:${storagePublicUrl(result.id, base)}`);
|
|
7926
|
+
}
|
|
7737
7927
|
return 0;
|
|
7738
7928
|
}
|
|
7739
7929
|
async function storageMove(action, token) {
|
|
@@ -7898,28 +8088,116 @@ async function storageCategories() {
|
|
|
7898
8088
|
return 0;
|
|
7899
8089
|
}
|
|
7900
8090
|
for (const row of rows)
|
|
7901
|
-
console.log(`${row.id} ${row.name} (${row.fileCount})`);
|
|
8091
|
+
console.log(`${row.id} ${row.name} [${row.color ?? "—"}] (${row.fileCount})`);
|
|
7902
8092
|
return 0;
|
|
7903
8093
|
}
|
|
7904
|
-
async function
|
|
8094
|
+
async function resolveCategoryId(credentials, idOrName) {
|
|
8095
|
+
const res = await api2(credentials.apiBase, "/v1/storage/categories", { headers: authHeaders(credentials) });
|
|
8096
|
+
if (res.status !== 200) {
|
|
8097
|
+
console.error(errorMessage(res.body, "获取分类失败。"));
|
|
8098
|
+
return exitCodeForApiError(res.body);
|
|
8099
|
+
}
|
|
8100
|
+
const rows = res.body.data.categories;
|
|
8101
|
+
const byId = rows.find((row) => row.id === idOrName);
|
|
8102
|
+
if (byId)
|
|
8103
|
+
return byId.id;
|
|
8104
|
+
const byName = rows.filter((row) => row.name === idOrName);
|
|
8105
|
+
if (byName.length === 1)
|
|
8106
|
+
return byName[0].id;
|
|
8107
|
+
if (byName.length === 0) {
|
|
8108
|
+
console.error(`分类不存在:${idOrName}(crab storage categories 查看列表)`);
|
|
8109
|
+
return EXIT_NOT_FOUND;
|
|
8110
|
+
}
|
|
8111
|
+
console.error(`分类名重复:${idOrName},请用 id 指认。`);
|
|
8112
|
+
return EXIT_USAGE;
|
|
8113
|
+
}
|
|
8114
|
+
function positionalArgs(rest, valuedFlags) {
|
|
8115
|
+
const out = [];
|
|
8116
|
+
for (let i = 0;i < rest.length; i++) {
|
|
8117
|
+
if (valuedFlags.includes(rest[i])) {
|
|
8118
|
+
i++;
|
|
8119
|
+
continue;
|
|
8120
|
+
}
|
|
8121
|
+
if (rest[i].startsWith("--"))
|
|
8122
|
+
continue;
|
|
8123
|
+
out.push(rest[i]);
|
|
8124
|
+
}
|
|
8125
|
+
return out;
|
|
8126
|
+
}
|
|
8127
|
+
async function storageMkCat(rest) {
|
|
8128
|
+
const name = positionalArgs(rest, ["--color"]).join(" ") || undefined;
|
|
7905
8129
|
if (!name) {
|
|
7906
|
-
console.error("用法:crab storage mkcat <名称>");
|
|
8130
|
+
console.error("用法:crab storage mkcat <名称> [--color 色]");
|
|
7907
8131
|
return EXIT_USAGE;
|
|
7908
8132
|
}
|
|
7909
8133
|
const credentials = requireCredentials();
|
|
7910
8134
|
if (typeof credentials === "number")
|
|
7911
8135
|
return credentials;
|
|
8136
|
+
const color = readFlag("--color");
|
|
7912
8137
|
const res = await api2(credentials.apiBase, "/v1/storage/categories", {
|
|
7913
8138
|
method: "POST",
|
|
7914
8139
|
headers: authHeaders(credentials),
|
|
7915
|
-
body: JSON.stringify({ name })
|
|
8140
|
+
body: JSON.stringify({ name, ...color ? { color } : {} })
|
|
7916
8141
|
});
|
|
7917
8142
|
if (res.status !== 200 && res.status !== 201) {
|
|
7918
8143
|
console.error(errorMessage(res.body, "创建分类失败。"));
|
|
7919
8144
|
return exitCodeForApiError(res.body);
|
|
7920
8145
|
}
|
|
7921
8146
|
const row = res.body.data;
|
|
7922
|
-
console.log(`✓ 已创建分类:${row.name}(id ${row.id})`);
|
|
8147
|
+
console.log(`✓ 已创建分类:${row.name}(id ${row.id},颜色 ${row.color ?? "—"})`);
|
|
8148
|
+
return 0;
|
|
8149
|
+
}
|
|
8150
|
+
async function storageRenCat(target, rest) {
|
|
8151
|
+
const name = positionalArgs(rest, ["--color"]).join(" ") || undefined;
|
|
8152
|
+
if (!target || !name && !readFlag("--color")) {
|
|
8153
|
+
console.error("用法:crab storage rencat <id|分类名> <新名称> [--color 色]");
|
|
8154
|
+
return EXIT_USAGE;
|
|
8155
|
+
}
|
|
8156
|
+
const credentials = requireCredentials();
|
|
8157
|
+
if (typeof credentials === "number")
|
|
8158
|
+
return credentials;
|
|
8159
|
+
const id = await resolveCategoryId(credentials, target);
|
|
8160
|
+
if (typeof id === "number")
|
|
8161
|
+
return id;
|
|
8162
|
+
const body = {};
|
|
8163
|
+
if (name)
|
|
8164
|
+
body.name = name;
|
|
8165
|
+
const color = readFlag("--color");
|
|
8166
|
+
if (color)
|
|
8167
|
+
body.color = color;
|
|
8168
|
+
const res = await api2(credentials.apiBase, `/v1/storage/categories/${encodeURIComponent(id)}`, {
|
|
8169
|
+
method: "PATCH",
|
|
8170
|
+
headers: authHeaders(credentials),
|
|
8171
|
+
body: JSON.stringify(body)
|
|
8172
|
+
});
|
|
8173
|
+
if (res.status !== 200) {
|
|
8174
|
+
console.error(errorMessage(res.body, "更新分类失败。"));
|
|
8175
|
+
return exitCodeForApiError(res.body);
|
|
8176
|
+
}
|
|
8177
|
+
const row = res.body.data;
|
|
8178
|
+
console.log(`✓ 分类已更新:${row.name}(颜色 ${row.color ?? "—"})`);
|
|
8179
|
+
return 0;
|
|
8180
|
+
}
|
|
8181
|
+
async function storageRmCat(target) {
|
|
8182
|
+
if (!target) {
|
|
8183
|
+
console.error("用法:crab storage rmcat <id|分类名>(分类下文件回落未分类,不删文件)");
|
|
8184
|
+
return EXIT_USAGE;
|
|
8185
|
+
}
|
|
8186
|
+
const credentials = requireCredentials();
|
|
8187
|
+
if (typeof credentials === "number")
|
|
8188
|
+
return credentials;
|
|
8189
|
+
const id = await resolveCategoryId(credentials, target);
|
|
8190
|
+
if (typeof id === "number")
|
|
8191
|
+
return id;
|
|
8192
|
+
const res = await api2(credentials.apiBase, `/v1/storage/categories/${encodeURIComponent(id)}`, {
|
|
8193
|
+
method: "DELETE",
|
|
8194
|
+
headers: authHeaders(credentials)
|
|
8195
|
+
});
|
|
8196
|
+
if (res.status !== 200) {
|
|
8197
|
+
console.error(errorMessage(res.body, "删除分类失败。"));
|
|
8198
|
+
return exitCodeForApiError(res.body);
|
|
8199
|
+
}
|
|
8200
|
+
console.log("✓ 分类已删除,其中文件回落未分类。");
|
|
7923
8201
|
return 0;
|
|
7924
8202
|
}
|
|
7925
8203
|
async function storageTags() {
|
|
@@ -8053,7 +8331,7 @@ async function collabSpaceCreate(name) {
|
|
|
8053
8331
|
return 0;
|
|
8054
8332
|
}
|
|
8055
8333
|
console.log(`✓ 独立协作空间已创建(id ${data.spaceId})`);
|
|
8056
|
-
console.log(`
|
|
8334
|
+
console.log(` 消耗:${fmtUsd(data.chargedCredits)} · --space space:${name} 即可指向它`);
|
|
8057
8335
|
return 0;
|
|
8058
8336
|
}
|
|
8059
8337
|
async function collabTasksList() {
|
|
@@ -8434,7 +8712,14 @@ async function init() {
|
|
|
8434
8712
|
return loginCode;
|
|
8435
8713
|
}
|
|
8436
8714
|
console.log("");
|
|
8437
|
-
|
|
8715
|
+
const code = installSkills(args.slice(1));
|
|
8716
|
+
const crabOnPath = (process.env.PATH ?? "").split(":").some((dir) => dir !== "" && !dir.includes("_npx") && existsSync2(join2(dir, "crab")));
|
|
8717
|
+
if (!crabOnPath) {
|
|
8718
|
+
console.log("");
|
|
8719
|
+
console.log("提示:`crab` 命令尚未全局安装——npm i -g @crabcloud/cli 可装到 PATH;");
|
|
8720
|
+
console.log(" 或所有命令直接 npx @crabcloud/cli <命令> 等价执行。");
|
|
8721
|
+
}
|
|
8722
|
+
return code;
|
|
8438
8723
|
}
|
|
8439
8724
|
async function logout() {
|
|
8440
8725
|
const credentials = loadCredentials();
|
|
@@ -8544,7 +8829,11 @@ switch (command) {
|
|
|
8544
8829
|
if (sub === "categories")
|
|
8545
8830
|
process.exit(await storageCategories());
|
|
8546
8831
|
if (sub === "mkcat")
|
|
8547
|
-
process.exit(await storageMkCat(args.slice(2)
|
|
8832
|
+
process.exit(await storageMkCat(args.slice(2)));
|
|
8833
|
+
if (sub === "rencat")
|
|
8834
|
+
process.exit(await storageRenCat(args[2], args.slice(3)));
|
|
8835
|
+
if (sub === "rmcat")
|
|
8836
|
+
process.exit(await storageRmCat(args[2]));
|
|
8548
8837
|
if (sub === "tags")
|
|
8549
8838
|
process.exit(await storageTags());
|
|
8550
8839
|
if (sub === "share")
|
|
@@ -8553,8 +8842,10 @@ switch (command) {
|
|
|
8553
8842
|
process.exit(await storageShares(args[2]));
|
|
8554
8843
|
if (sub === "unshare")
|
|
8555
8844
|
process.exit(await storageUnshare(args[2]));
|
|
8845
|
+
if (sub === "key")
|
|
8846
|
+
process.exit(await storageKeyCmd());
|
|
8556
8847
|
console.error(sub ? `未知子命令:storage ${sub}
|
|
8557
|
-
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | tags | share | shares | unshare
|
|
8848
|
+
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | rencat | rmcat | tags | share | shares | unshare | key
|
|
8558
8849
|
`);
|
|
8559
8850
|
console.log(usage);
|
|
8560
8851
|
process.exit(EXIT_USAGE);
|
|
@@ -15,9 +15,10 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
|
|
|
15
15
|
|
|
16
16
|
## 第一步:确认 CLI 可用
|
|
17
17
|
|
|
18
|
-
- 运行 `crab --version`
|
|
19
|
-
`
|
|
20
|
-
|
|
18
|
+
- 运行 `crab --version` 确认已安装。注意 `crab init` 只做登录 + 安装 skill,
|
|
19
|
+
**不含命令本体**——本机没有 `crab` 命令时让用户 `npm i -g @crabcloud/cli@latest`
|
|
20
|
+
安装(已装旧版同款命令升级);不想装全局则整段跳过,下文所有 `crab <命令>`
|
|
21
|
+
直接 `npx @crabcloud/cli <命令>` 等价执行(init 同样可 `npx @crabcloud/cli init`)。
|
|
21
22
|
- `crab whoami` 报「未登录」或退出码 3 → 引导用户 `crab login`。登录走浏览器
|
|
22
23
|
设备授权——授权页摆出全目录供用户勾选,**实际授予集由人定**,可能多于或少于
|
|
23
24
|
`--scopes` 请求——**你只发起,不代批**;登录后先 `crab whoami` 核对实际
|
|
@@ -13,9 +13,10 @@ skill 覆盖协作域的 agent 通道;账号/令牌管理见 `crabcloud` skill
|
|
|
13
13
|
|
|
14
14
|
## 第一步:确认可用
|
|
15
15
|
|
|
16
|
-
- 运行 `crab --version`
|
|
17
|
-
`
|
|
18
|
-
|
|
16
|
+
- 运行 `crab --version` 确认已安装。注意 `crab init` 只做登录 + 安装 skill,
|
|
17
|
+
**不含命令本体**——本机没有 `crab` 命令时让用户 `npm i -g @crabcloud/cli@latest`
|
|
18
|
+
安装(已装旧版同款命令升级);不想装全局则整段跳过,下文所有 `crab <命令>`
|
|
19
|
+
直接 `npx @crabcloud/cli <命令>` 等价执行(init 同样可 `npx @crabcloud/cli init`)。
|
|
19
20
|
- CLI 基于 npx 分发,**本机没有 `crab` 命令时,下文所有 `crab <命令>` 都可以
|
|
20
21
|
`npx @crabcloud/cli <命令>` 等价执行**(例如 `npx @crabcloud/cli collab tasks`)。
|
|
21
22
|
- 未登录(退出码 3)引导用户 `crab login`。
|
|
@@ -12,9 +12,10 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
|
|
|
12
12
|
|
|
13
13
|
## 第一步:确认可用
|
|
14
14
|
|
|
15
|
-
- 运行 `crab --version`
|
|
16
|
-
`
|
|
17
|
-
|
|
15
|
+
- 运行 `crab --version` 确认已安装。注意 `crab init` 只做登录 + 安装 skill,
|
|
16
|
+
**不含命令本体**——本机没有 `crab` 命令时让用户 `npm i -g @crabcloud/cli@latest`
|
|
17
|
+
安装(已装旧版同款命令升级);不想装全局则整段跳过,下文所有 `crab <命令>`
|
|
18
|
+
直接 `npx @crabcloud/cli <命令>` 等价执行(init 同样可 `npx @crabcloud/cli init`)。
|
|
18
19
|
- CLI 基于 npx 分发,**本机没有 `crab` 命令时,下文所有 `crab <命令>` 都可以
|
|
19
20
|
`npx @crabcloud/cli <命令>` 等价执行**(例如 `npx @crabcloud/cli mail list`)。
|
|
20
21
|
- 未登录(退出码 3)引导用户 `crab login`。
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: crabcloud-storage
|
|
3
|
-
description: Manage the user's Crab Cloud Drive (云盘 — unified storage for drive uploads, media assets and mail attachments on one quota) through the `crab` CLI — list and filter objects, upload and download files, rename, tag, categorize, publish/unpublish public links, trash/restore/purge, and check the storage quota. Use this skill when the user asks to upload, find, organize, share or clean up files on Crab Cloud ("上传这个文件", "云盘里有什么", "把这个文件公开分享", "看看存储用了多少", "crab storage"), or wants files moved in or out of their drive — even if they never say "crab" or "Crab Cloud" explicitly.
|
|
3
|
+
description: Manage the user's Crab Cloud Drive (云盘 — unified storage for drive uploads, media assets and mail attachments on one quota, with end-to-end encryption) through the `crab` CLI — list and filter objects, upload and download files (including decrypting share links), rename, tag, categorize, publish/unpublish public links, create expiring share links with passcodes, manage the locally remembered encryption key, trash/restore/purge, and check the storage quota. Use this skill when the user asks to upload, find, organize, share or clean up files on Crab Cloud ("上传这个文件", "云盘里有什么", "把这个文件公开分享", "看看存储用了多少", "crab storage"), or wants files moved in or out of their drive — even if they never say "crab" or "Crab Cloud" explicitly.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Crab Cloud 云盘(crab storage)
|
|
7
7
|
|
|
8
8
|
云盘是用户的统一存储:云盘上传 / 素材库 / 邮件附件三来源共用一本 2 GB 容量账;
|
|
9
|
-
属性优先、位置无关——分类(至多一个)+ 标签(AND
|
|
10
|
-
|
|
11
|
-
`crabcloud-mail`
|
|
9
|
+
属性优先、位置无关——分类(至多一个)+ 标签(AND 检索)代替文件夹。files 域
|
|
10
|
+
上传默认端到端加密(服务器只有包裹态密钥,明文密钥只在本机)。本 skill 覆盖
|
|
11
|
+
云盘域的 agent 通道;账号/令牌管理见 `crabcloud` skill,邮箱见 `crabcloud-mail`
|
|
12
|
+
skill。
|
|
12
13
|
|
|
13
14
|
## 第一步:确认可用
|
|
14
15
|
|
|
15
|
-
- 运行 `crab --version`
|
|
16
|
-
`
|
|
17
|
-
|
|
16
|
+
- 运行 `crab --version` 确认已安装。注意 `crab init` 只做登录 + 安装 skill,
|
|
17
|
+
**不含命令本体**——本机没有 `crab` 命令时让用户 `npm i -g @crabcloud/cli@latest`
|
|
18
|
+
安装(已装旧版同款命令升级);不想装全局则整段跳过,下文所有 `crab <命令>`
|
|
19
|
+
直接 `npx @crabcloud/cli <命令>` 等价执行(init 同样可 `npx @crabcloud/cli init`)。
|
|
18
20
|
- CLI 基于 npx 分发,**本机没有 `crab` 命令时,下文所有 `crab <命令>` 都可以
|
|
19
21
|
`npx @crabcloud/cli <命令>` 等价执行**(例如 `npx @crabcloud/cli storage ls`)。
|
|
20
22
|
- 未登录(退出码 3)引导用户 `crab login`。
|
|
@@ -29,22 +31,27 @@ description: Manage the user's Crab Cloud Drive (云盘 — unified storage for
|
|
|
29
31
|
|
|
30
32
|
| 用户意图 | 命令 |
|
|
31
33
|
|---|---|
|
|
32
|
-
| 云盘里有什么 | `crab storage ls`(按时间倒序,默认 30
|
|
33
|
-
| 找文件 | `crab storage ls --q 关键字`(文件名匹配;可叠 `--source files\|media\|mail`、`--type doc\|image\|video\|archive\|code`、`--tag a,b`、`--shared`、`--recent
|
|
34
|
+
| 云盘里有什么 | `crab storage ls`(按时间倒序,默认 30 条;`--limit` / `--offset` 翻页) |
|
|
35
|
+
| 找文件 | `crab storage ls --q 关键字`(文件名匹配;可叠 `--source files\|media\|mail`、`--type doc\|image\|video\|archive\|code`、`--category 分类id\|none`、`--tag a,b`、`--shared`、`--recent`、`--message-id <id>`) |
|
|
34
36
|
| 看回收站 | `crab storage ls --trash` |
|
|
35
|
-
| 上传文件 | `crab storage upload
|
|
36
|
-
| 下载文件 | `crab storage download <id
|
|
37
|
+
| 上传文件 | `crab storage upload <路径...>`(多文件串行;直传优先,自动回退中转;默认端到端加密——口令三来源见下节,首次使用自动初始化并打印一次性恢复码) |
|
|
38
|
+
| 下载文件 | `crab storage download <id\|文件名\|分享链接> [-o 输出路径] [--passcode 提取码]`(默认按原文件名存当前目录;**凭完整分享链接可免登录下载并本地解密**——`#` 后是密钥片段,这是 agent→agent 的主通道;带提取码的分享加 `--passcode`) |
|
|
37
39
|
| 改名 | `crab storage rename <id\|文件名> <新名称>` |
|
|
38
40
|
| 加/删标签 | `crab storage tag <id\|文件名> --add a,b [--remove c,d]` |
|
|
39
|
-
| 归类
|
|
41
|
+
| 归类 | `crab storage category <id\|文件名> <分类id\|分类名\|none>`(分类名唯一命中即解析) |
|
|
42
|
+
| 建分类 | `crab storage mkcat <名称> [--color 色]` |
|
|
43
|
+
| 改分类名/颜色 | `crab storage rencat <id\|分类名> <新名称> [--color 色]` |
|
|
44
|
+
| 删分类 | `crab storage rmcat <id\|分类名>`(分类下文件回落未分类,不删文件) |
|
|
40
45
|
| 公开分享 | `crab storage publish <id\|文件名>`(打印公开直链;收回用 `unpublish`) |
|
|
41
46
|
| 可控分享(有效期/提取码/撤销) | `crab storage share <id\|文件名> [--expires 7\|30\|90\|forever] [--passcode 提取码]`(打印完整分享链接;加密对象链接自带 `#` 密钥片段);列表 `crab storage shares <id\|文件名>`、撤销 `crab storage unshare <shareId>` |
|
|
47
|
+
| 云盘钥匙管理 | `crab storage key`(查状态)/ `--remember`(解锁一次并记住,之后免口令)/ `--forget`(清除)——见下节 |
|
|
42
48
|
| 删除 | `crab storage rm <id\|文件名>`(进回收站,7 天后自动清理)→ `restore` 恢复 / `purge` 彻底删除 |
|
|
43
49
|
| 看水位 | `crab storage quota`(三来源分项 + 回收站占用 + 对象计数) |
|
|
44
50
|
|
|
45
51
|
`<id|文件名>`:文件名全库精确唯一命中时自动解析为 id(回收站内同样可解析,
|
|
46
52
|
restore/purge 直接用文件名即可);命中多个或查不到时按字面当 id 用——拿不准
|
|
47
|
-
先 `crab storage ls --q <关键字>`
|
|
53
|
+
先 `crab storage ls --q <关键字>` 核对再操作。分类指认同款:id 精确命中优先,
|
|
54
|
+
名称唯一命中其次(`rencat` / `rmcat` / `--category` 都吃 `<id|分类名>`)。
|
|
48
55
|
|
|
49
56
|
## 机器可读输出
|
|
50
57
|
|
|
@@ -53,7 +60,26 @@ restore/purge 直接用文件名即可);命中多个或查不到时按字面
|
|
|
53
60
|
- `storage ls --json` → `{ objects: [...], nextOffset, total }`,条目含
|
|
54
61
|
`id / source / filename / mimeType / sizeBytes / categoryId / tags / isPublic / createdAt`;
|
|
55
62
|
- `storage quota --json` → `{ usedBytes, quotaBytes, bySource: { files, media, mail }, trashBytes, counts }`;
|
|
56
|
-
- `upload` / `categories` / `tags` 的 `--json` 同构(单对象 / 分类数组 /
|
|
63
|
+
- `upload` / `categories` / `tags` / `shares` 的 `--json` 同构(单对象 / 分类数组 /
|
|
64
|
+
标签数组 / 分享数组)。
|
|
65
|
+
|
|
66
|
+
## 云盘口令与本机钥匙
|
|
67
|
+
|
|
68
|
+
files 域是端到端加密:服务器只有口令包裹态的主密钥,**明文密钥只存在于用户
|
|
69
|
+
本机**——CLI 不可能也不允许向服务器索取。涉及加密对象的命令(upload /
|
|
70
|
+
download / share 加密文件)需要云盘口令,来源优先级:
|
|
71
|
+
|
|
72
|
+
1. `--passphrase <口令>`(flag)
|
|
73
|
+
2. 环境变量 `CRAB_PASSPHRASE`(**Agent/脚本推荐**——非交互终端不会弹提问,
|
|
74
|
+
缺失时明确报错)
|
|
75
|
+
3. 交互终端(TTY)隐藏输入提问(人不经管道跑 CLI 时的默认体验)
|
|
76
|
+
|
|
77
|
+
本机钥匙(`crab storage key`):口令解锁成功后可把**主密钥**(非口令)记住到
|
|
78
|
+
本机 `~/.config/crabcloud/drive-key-<账号id>.json`(0600,按账号+环境隔离),
|
|
79
|
+
之后所有命令免口令——等同 Web 端「记住此设备」。`--remember` 主动记住、
|
|
80
|
+
`--forget` 清除、默认查状态;Web 端轮换口令/恢复码后本机钥匙自动验签失效并
|
|
81
|
+
回落口令,无需手工干预。Agent 视角:**用户没让你动钥匙设置时不要主动调
|
|
82
|
+
`storage key`**;用户口令遗忘是恢复码流程(Web 端),CLI 侧无法代找回。
|
|
57
83
|
|
|
58
84
|
## 纪律
|
|
59
85
|
|
|
@@ -61,7 +87,11 @@ restore/purge 直接用文件名即可);命中多个或查不到时按字面
|
|
|
61
87
|
确认后再执行;收回用 `unpublish`(已缓存浏览器至多 5 分钟内失效)。
|
|
62
88
|
- **share 是可控分享**:默认 7 天有效、可选提取码、随时 `unshare` 撤销;加密
|
|
63
89
|
对象的分享链接自带解密钥匙(`#` 后缀)——泄露链接即泄露文件,外发前向
|
|
64
|
-
|
|
90
|
+
用户复述有效期与提取码设置,确认后再执行。收件人在浏览器打开加密分享链接
|
|
91
|
+
会看到内联解密落地页(图片/视频/音频/PDF/文本直接预览,其余类型仅下载)。
|
|
92
|
+
- **提取码错误重试有服务端限流**:同一链接+来源 IP 错误次数超限返回 429
|
|
93
|
+
(退出码 1,消息「尝试过于频繁」)——**不要程序化暴力重试**,如实转述用户
|
|
94
|
+
等待即可;凭链接下载时提取码用 `--passcode` 正确携带。
|
|
65
95
|
- **purge 是彻底删除**,不可恢复;用户没明说「彻底删除」时一律用 `rm`
|
|
66
96
|
(回收站保留 7 天可找回)。
|
|
67
97
|
- 邮件附件与用户上传同账:水位告急时用 `quota` 分项定位来源再建议清理。
|
package/package.json
CHANGED