@crabcloud/cli 0.1.10 → 0.2.1
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 +882 -39
- package/dist/skills/crabcloud-collab/SKILL.md +95 -0
- package/dist/skills/crabcloud-storage/SKILL.md +41 -12
- package/package.json +2 -2
package/dist/crab.js
CHANGED
|
@@ -16,6 +16,8 @@ var __export = (target, all) => {
|
|
|
16
16
|
|
|
17
17
|
// src/index.ts
|
|
18
18
|
import { hostname, homedir as homedir2 } from "node:os";
|
|
19
|
+
import { createInterface } from "node:readline";
|
|
20
|
+
import { Writable } from "node:stream";
|
|
19
21
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
20
22
|
import { basename, join as join2 } from "node:path";
|
|
21
23
|
|
|
@@ -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,7 @@ var collabAllocationInput = object({
|
|
|
6192
6194
|
});
|
|
6193
6195
|
var collabBoostInput = object({ kind: _enum(collabBoostKinds) });
|
|
6194
6196
|
var collabInviteInput = object({ username: usernameSchema });
|
|
6195
|
-
var collabLinkInput = string2().trim().url().max(2000);
|
|
6197
|
+
var collabLinkInput = string2().trim().url().max(2000).regex(/^https?:\/\//i);
|
|
6196
6198
|
var collabCreateTaskInput = object({
|
|
6197
6199
|
title: string2().trim().min(1).max(COLLAB_TITLE_MAX),
|
|
6198
6200
|
body: string2().max(COLLAB_RECORD_BODY_MAX).default(""),
|
|
@@ -6451,6 +6453,7 @@ function stripCodexSection(content) {
|
|
|
6451
6453
|
var DEFAULT_API = "https://api.crabcloud.cc";
|
|
6452
6454
|
var DEFAULT_SCOPES = ["account.read"];
|
|
6453
6455
|
var CONFIG_PATH = process.env.CRAB_CREDENTIALS ?? join2(homedir2(), ".config", "crabcloud", "credentials.json");
|
|
6456
|
+
var CONFIG_DIR = join2(homedir2(), ".config", "crabcloud");
|
|
6454
6457
|
var EXIT_USAGE = 2;
|
|
6455
6458
|
var EXIT_AUTH = 3;
|
|
6456
6459
|
var EXIT_SCOPE = 4;
|
|
@@ -6503,6 +6506,9 @@ function exitCodeForApiError(body) {
|
|
|
6503
6506
|
case "insufficient_credits":
|
|
6504
6507
|
case "storage.quotaExceeded":
|
|
6505
6508
|
return EXIT_QUOTA;
|
|
6509
|
+
case "collab_space_not_bound":
|
|
6510
|
+
case "collab_space_mismatch":
|
|
6511
|
+
return EXIT_SCOPE;
|
|
6506
6512
|
default:
|
|
6507
6513
|
return 1;
|
|
6508
6514
|
}
|
|
@@ -6537,7 +6543,10 @@ function formatTime(ms) {
|
|
|
6537
6543
|
return new Date(ms).toLocaleString("sv", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
6538
6544
|
}
|
|
6539
6545
|
function fmtCredits(cents) {
|
|
6540
|
-
return cents.toLocaleString("en-US");
|
|
6546
|
+
return (cents / 100).toLocaleString("en-US");
|
|
6547
|
+
}
|
|
6548
|
+
function fmtUsd(cents) {
|
|
6549
|
+
return `$${cents / 100}`;
|
|
6541
6550
|
}
|
|
6542
6551
|
var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
6543
6552
|
发布物经 npm 分发:npx @crabcloud/cli <command>(全局安装后短名 crab 等价)
|
|
@@ -6550,6 +6559,9 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6550
6559
|
login 浏览器授权登录(设备授权流),保存 Agent Token
|
|
6551
6560
|
--scopes <scope|组名,...> 自定义 scope,支持组名整组授权
|
|
6552
6561
|
(默认 ${DEFAULT_SCOPES.join(",")})
|
|
6562
|
+
--space <default|space:<名称>>
|
|
6563
|
+
含协作 scope 时必填:令牌绑定该协作
|
|
6564
|
+
空间,collab 命令只能作用于它
|
|
6553
6565
|
--no-open 只打印链接,不自动打开浏览器
|
|
6554
6566
|
--api <url> 指定 API 地址
|
|
6555
6567
|
|
|
@@ -6580,14 +6592,17 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6580
6592
|
[--type doc|image|video|archive|code] [--tag a,b] [--trash]
|
|
6581
6593
|
[--shared] [--recent] [--message-id <id>] [--limit N] [--offset N] [--json]
|
|
6582
6594
|
列出云盘对象(跨来源统一视图,能力与 Web 云盘一致)
|
|
6583
|
-
storage upload <file...> [--json]
|
|
6595
|
+
storage upload <file...> [--json] 上传一个或多个文件(直传优先,自动回退中转);
|
|
6596
|
+
files 域默认端到端加密,口令 --passphrase /
|
|
6597
|
+
CRAB_PASSPHRASE 提供,终端内运行时交互输入
|
|
6584
6598
|
storage download <id|文件名> [-o 输出路径]
|
|
6585
|
-
|
|
6599
|
+
下载对象内容(默认按原文件名存到当前目录;
|
|
6600
|
+
加密对象需口令,终端内运行时交互输入)
|
|
6586
6601
|
storage rename <id|文件名> <新名称> 重命名
|
|
6587
6602
|
storage tag <id|文件名> --add a,b [--remove c,d]
|
|
6588
6603
|
增删标签
|
|
6589
|
-
storage category <id|文件名>
|
|
6590
|
-
|
|
6604
|
+
storage category <id|文件名> <分类id|分类名|none>
|
|
6605
|
+
设置分类(分类见 storage categories)
|
|
6591
6606
|
storage publish|unpublish <id|文件名>
|
|
6592
6607
|
公开 / 收回公开(公开时打印公开直链)
|
|
6593
6608
|
storage rm|restore|purge <id|文件名>
|
|
@@ -6595,12 +6610,42 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6595
6610
|
storage quota [--json] 存储水位(统一容量账:文件/素材/邮件附件一本账)
|
|
6596
6611
|
storage share <id|文件名> [--expires 7|30|90|forever] [--passcode 提取码]
|
|
6597
6612
|
创建分享链接(加密文件链接自带解密钥匙 #fragment)
|
|
6613
|
+
storage key [--remember|--forget]
|
|
6614
|
+
本机记住的云盘钥匙管理:默认查看状态;--remember 解锁一次
|
|
6615
|
+
并记住(下次免输口令);--forget 清除
|
|
6598
6616
|
storage shares <id|文件名> [--json]
|
|
6599
6617
|
该文件的分享链接列表(状态/下载计数)
|
|
6600
6618
|
storage unshare <shareId> 撤销分享
|
|
6601
6619
|
storage categories [--json] 分类列表
|
|
6602
|
-
storage mkcat <名称>
|
|
6620
|
+
storage mkcat <名称> [--color 色] 新建分类(颜色缺省 action)
|
|
6621
|
+
storage rencat <id|分类名> <新名称> [--color 色]
|
|
6622
|
+
分类重命名 / 改色
|
|
6623
|
+
storage rmcat <id|分类名> 删除分类(其中文件回落未分类)
|
|
6603
6624
|
storage tags [--json] 标签汇总
|
|
6625
|
+
collab spaces [--json] 列出我所在的协作空间(任务/成员计数与限额)
|
|
6626
|
+
collab space create <名称> [--allocation-mb N] [--json]
|
|
6627
|
+
创建独立协作空间(消耗 5 积分;默认自分配 500MB
|
|
6628
|
+
存储占订阅总池,可随时调)
|
|
6629
|
+
collab tasks [--space <arg>] [--status open|done] [--q 关键字]
|
|
6630
|
+
[--limit N] [--cursor <cursor>] [--json]
|
|
6631
|
+
列出任务(含摘要/记录数;--q 搜标题与记录正文)
|
|
6632
|
+
collab task <id> [--json] 任务详情
|
|
6633
|
+
collab task create --space <arg> --title "..." [--body "..." | --body-file <path>]
|
|
6634
|
+
[--attach <fileId,...>] [--link <url,...>] [--json]
|
|
6635
|
+
新建任务(首条记录即 body 入流)
|
|
6636
|
+
collab task records <id> [--limit N] [--cursor <cursor>] [--json]
|
|
6637
|
+
读任务记录流(倒序,Markdown 源文 + 附件/链接)
|
|
6638
|
+
collab task done|reopen <id> 完成 / 重开(入流一条状态变更,占记录位)
|
|
6639
|
+
collab task summary <id> (--text "..." | --text-file <path> | --clear) [--json]
|
|
6640
|
+
更新任务摘要(可变「当前态」概览,不入记录流;
|
|
6641
|
+
上限 2000 字符,--clear 清空)
|
|
6642
|
+
collab members [--space <arg>] [--json]
|
|
6643
|
+
协作空间成员列表
|
|
6644
|
+
collab invite <username> [--space <arg>] [--json]
|
|
6645
|
+
邀请联系人中已关联的平台用户进空间
|
|
6646
|
+
collab activity [--json] 协作域活动事件(Agent 推送/读取、任务创建、摘要更新)
|
|
6647
|
+
post <taskId> (--body "..." | --body-file <path>) [--attach <fileId,...>] [--link <url,...>] [--json]
|
|
6648
|
+
推送任务记录(糖命令:等价 collab task records 的写入)
|
|
6604
6649
|
credits [--json] 积分余额(月送/充值/合计;需 credits.read scope)
|
|
6605
6650
|
credits history [--limit N] [--offset N] [--json]
|
|
6606
6651
|
积分流水(变动 ±:消耗为负;需 credits.read)
|
|
@@ -6618,7 +6663,11 @@ ${scopeGroups.map((group) => ` ${group.id.padEnd(9)} ${group.scopes.join(", ")}
|
|
|
6618
6663
|
CRAB_API API 地址(默认 ${DEFAULT_API})
|
|
6619
6664
|
CRAB_CREDENTIALS 凭证文件路径(默认 ~/.config/crabcloud/credentials.json)
|
|
6620
6665
|
|
|
6621
|
-
|
|
6666
|
+
collab 命令的 <arg>(--space 值): default(默认空间)| space:<名称>(按名唯一命中)| 空间 id;
|
|
6667
|
+
缺省时你只在一个空间则自动采用,多个则列出候选要求显式指定。Agent 令牌绑定空间后,
|
|
6668
|
+
越界访问一律 403(collab_space_mismatch)。
|
|
6669
|
+
|
|
6670
|
+
退出码: 0 成功 · 2 用法错误 · 3 认证失效 · 4 scope 不足(含令牌未绑/绑错协作空间)· 5 资源不存在 · 7 配额/积分不足
|
|
6622
6671
|
`;
|
|
6623
6672
|
async function login() {
|
|
6624
6673
|
const apiBase = process.env.CRAB_API ?? readFlag("--api") ?? DEFAULT_API;
|
|
@@ -6634,10 +6683,16 @@ async function login() {
|
|
|
6634
6683
|
console.error(`--scopes 展开后为空(输入:${rawScopes.join(", ")})`);
|
|
6635
6684
|
return EXIT_USAGE;
|
|
6636
6685
|
}
|
|
6686
|
+
const spaceArg = readFlag("--space");
|
|
6687
|
+
if (requested.some((s) => s === "collab.read" || s === "collab.write") && !spaceArg) {
|
|
6688
|
+
console.error("含协作 scope(collab)时必须指定 --space <default|space:<名称>>——令牌将绑定该协作空间,越界访问一律拒绝。");
|
|
6689
|
+
console.error("示例:crab login --scopes collab --space default");
|
|
6690
|
+
return EXIT_USAGE;
|
|
6691
|
+
}
|
|
6637
6692
|
const openBrowser = !hasFlag("--no-open");
|
|
6638
6693
|
const start = await api2(apiBase, "/v1/auth/device/start", {
|
|
6639
6694
|
method: "POST",
|
|
6640
|
-
body: JSON.stringify({ name: `${hostname()} · crab cli`, scopes: requested })
|
|
6695
|
+
body: JSON.stringify({ name: `${hostname()} · crab cli`, scopes: requested, ...spaceArg ? { space: spaceArg } : {} })
|
|
6641
6696
|
});
|
|
6642
6697
|
if (start.status !== 200) {
|
|
6643
6698
|
console.error(`发起授权失败(${start.status}):`, errorMessage(start.body, JSON.stringify(start.body)));
|
|
@@ -6679,6 +6734,8 @@ async function login() {
|
|
|
6679
6734
|
saveCredentials({ apiBase, token: poll.body.data.token, scopes: poll.body.data.scopes });
|
|
6680
6735
|
console.log(`✓ 已登录,Agent Token 已保存到 ${CONFIG_PATH}`);
|
|
6681
6736
|
console.log(` scopes: ${poll.body.data.scopes.join(", ")}`);
|
|
6737
|
+
if (spaceArg)
|
|
6738
|
+
console.log(` 协作空间绑定: ${spaceArg}(collab 命令只能作用于该空间)`);
|
|
6682
6739
|
return 0;
|
|
6683
6740
|
}
|
|
6684
6741
|
}
|
|
@@ -6779,7 +6836,7 @@ async function creditsHistory() {
|
|
|
6779
6836
|
console.log(`${formatTime(entry.createdAt)} ${CREDIT_REASON_LABELS[entry.reason] ?? entry.reason} ${signed}`);
|
|
6780
6837
|
}
|
|
6781
6838
|
console.log(`
|
|
6782
|
-
共 ${
|
|
6839
|
+
共 ${data.total.toLocaleString("en-US")} 条(本页 ${data.ledger.length} 条,--offset 翻页)`);
|
|
6783
6840
|
return 0;
|
|
6784
6841
|
}
|
|
6785
6842
|
async function creditsPricing() {
|
|
@@ -6799,10 +6856,10 @@ async function creditsPricing() {
|
|
|
6799
6856
|
console.log("暂无动作计价项。");
|
|
6800
6857
|
return 0;
|
|
6801
6858
|
}
|
|
6802
|
-
console.log("动作计价(
|
|
6859
|
+
console.log("动作计价(1 积分 = $1,价目为 $ 口径):");
|
|
6803
6860
|
for (const item of actions) {
|
|
6804
6861
|
const label = CREDIT_ACTION_LABELS[item.action] ?? item.action;
|
|
6805
|
-
console.log(` ${item.action.padEnd(24)} ${label} ${
|
|
6862
|
+
console.log(` ${item.action.padEnd(24)} ${label} ${fmtUsd(item.creditCents)}`);
|
|
6806
6863
|
}
|
|
6807
6864
|
return 0;
|
|
6808
6865
|
}
|
|
@@ -6825,7 +6882,7 @@ async function tokensList() {
|
|
|
6825
6882
|
return 0;
|
|
6826
6883
|
}
|
|
6827
6884
|
for (const token of rows) {
|
|
6828
|
-
console.log(`${token.id} ${token.name} [${token.scopes.join(", ")}] ${token.revoked ? "已撤销" : "生效中"}`);
|
|
6885
|
+
console.log(`${token.id} ${token.name} [${token.scopes.join(", ")}] ${token.revoked ? "已撤销" : "生效中"}${token.collabSpace ? ` ⟨空间 ${token.collabSpace}⟩` : ""}`);
|
|
6829
6886
|
console.log(` 创建 ${formatTime(token.createdAt)} · 最近使用 ${token.lastUsedAt ? formatTime(token.lastUsedAt) : "—"}${token.expiresAt ? ` · 过期 ${formatTime(token.expiresAt)}` : ""}`);
|
|
6830
6887
|
}
|
|
6831
6888
|
return 0;
|
|
@@ -7080,7 +7137,7 @@ async function mailSend() {
|
|
|
7080
7137
|
if (attachmentIds.length > 0)
|
|
7081
7138
|
console.log(` 附件 ${attachmentIds.length} 个(云盘引用,不复制)`);
|
|
7082
7139
|
if (result.chargedCredits > 0)
|
|
7083
|
-
console.log(`
|
|
7140
|
+
console.log(` 消耗:${fmtUsd(result.chargedCredits)}`);
|
|
7084
7141
|
if (result.failedExternal > 0)
|
|
7085
7142
|
console.log(` ⚠ ${result.failedExternal} 个外部收件人发送失败,对应积分已退回`);
|
|
7086
7143
|
return 0;
|
|
@@ -7402,13 +7459,168 @@ var utf82 = (s) => new TextEncoder().encode(s);
|
|
|
7402
7459
|
var MASTER_AAD = utf82("drive-master");
|
|
7403
7460
|
var DATA_AAD = utf82("drive-data");
|
|
7404
7461
|
var KEK_ITERATIONS = 210000;
|
|
7405
|
-
|
|
7406
|
-
|
|
7462
|
+
var passphraseMemo;
|
|
7463
|
+
async function drivePassphrase() {
|
|
7464
|
+
if (passphraseMemo !== undefined)
|
|
7465
|
+
return passphraseMemo;
|
|
7466
|
+
const explicit = readFlag("--passphrase") ?? process.env.CRAB_PASSPHRASE;
|
|
7467
|
+
if (explicit !== undefined && explicit !== "") {
|
|
7468
|
+
passphraseMemo = explicit;
|
|
7469
|
+
return explicit;
|
|
7470
|
+
}
|
|
7471
|
+
if (!process.stdin.isTTY)
|
|
7472
|
+
return;
|
|
7473
|
+
process.stderr.write("请输入云盘口令(输入不回显)Drive passphrase: ");
|
|
7474
|
+
const typed = await askHidden();
|
|
7475
|
+
if (typed.length === 0)
|
|
7476
|
+
return;
|
|
7477
|
+
passphraseMemo = typed;
|
|
7478
|
+
return typed;
|
|
7479
|
+
}
|
|
7480
|
+
function askHidden() {
|
|
7481
|
+
return new Promise((resolve) => {
|
|
7482
|
+
const silent = new Writable({ write(_chunk, _enc, cb) {
|
|
7483
|
+
cb();
|
|
7484
|
+
} });
|
|
7485
|
+
const rl = createInterface({ input: process.stdin, output: silent, terminal: true });
|
|
7486
|
+
rl.question("", (answer) => {
|
|
7487
|
+
rl.close();
|
|
7488
|
+
process.stderr.write(`
|
|
7489
|
+
`);
|
|
7490
|
+
resolve(answer);
|
|
7491
|
+
});
|
|
7492
|
+
});
|
|
7407
7493
|
}
|
|
7408
|
-
|
|
7409
|
-
|
|
7494
|
+
function askVisible(question) {
|
|
7495
|
+
return new Promise((resolve) => {
|
|
7496
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
7497
|
+
rl.question(question, (answer) => {
|
|
7498
|
+
rl.close();
|
|
7499
|
+
resolve(answer);
|
|
7500
|
+
});
|
|
7501
|
+
});
|
|
7502
|
+
}
|
|
7503
|
+
var CANARY_PLAIN = utf82("crab-drive-verify");
|
|
7504
|
+
var CANARY_AAD = utf82("drive-verify");
|
|
7505
|
+
function driveKeyPath(accountId) {
|
|
7506
|
+
return join2(CONFIG_DIR, `drive-key-${accountId}.json`);
|
|
7507
|
+
}
|
|
7508
|
+
function readDriveKeyFile(accountId) {
|
|
7509
|
+
try {
|
|
7510
|
+
return JSON.parse(readFileSync2(driveKeyPath(accountId), "utf8"));
|
|
7511
|
+
} catch {
|
|
7512
|
+
return null;
|
|
7513
|
+
}
|
|
7514
|
+
}
|
|
7515
|
+
async function loadVerifiedDriveKey(credentials, accountId) {
|
|
7516
|
+
const file = readDriveKeyFile(accountId);
|
|
7517
|
+
if (!file || file.apiBase !== credentials.apiBase || typeof file.masterKey !== "string" || typeof file.canary !== "string") {
|
|
7518
|
+
return null;
|
|
7519
|
+
}
|
|
7520
|
+
try {
|
|
7521
|
+
const key = await importAesKey(b64urlDecode(file.masterKey));
|
|
7522
|
+
await aeadDecrypt(key, sealFromB64url(file.canary), CANARY_AAD);
|
|
7523
|
+
return b64urlDecode(file.masterKey);
|
|
7524
|
+
} catch {
|
|
7525
|
+
forgetDriveKey(accountId);
|
|
7526
|
+
console.error("本机记住的云盘钥匙已失效(可能在别处轮换),请改用口令。");
|
|
7527
|
+
return null;
|
|
7528
|
+
}
|
|
7529
|
+
}
|
|
7530
|
+
async function storeDriveKey(accountId, apiBase, masterRaw) {
|
|
7531
|
+
const sealed = await aeadEncrypt(await importAesKey(masterRaw), CANARY_PLAIN, CANARY_AAD);
|
|
7532
|
+
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
7533
|
+
writeFileSync2(driveKeyPath(accountId), `${JSON.stringify({ apiBase, masterKey: b64urlEncode(masterRaw), canary: sealToB64url({ iv: sealed.iv, ciphertext: sealed.ciphertext }), storedAt: Date.now() }, null, 2)}
|
|
7534
|
+
`, { mode: 384 });
|
|
7535
|
+
}
|
|
7536
|
+
function forgetDriveKey(accountId) {
|
|
7537
|
+
rmSync2(driveKeyPath(accountId), { force: true });
|
|
7538
|
+
}
|
|
7539
|
+
var accountIdMemo;
|
|
7540
|
+
async function driveAccountId(credentials) {
|
|
7541
|
+
if (accountIdMemo !== undefined)
|
|
7542
|
+
return accountIdMemo;
|
|
7543
|
+
const res = await api2(credentials.apiBase, "/v1/auth/me", { headers: authHeaders(credentials) });
|
|
7544
|
+
accountIdMemo = res.status === 200 ? res.body.data.account.id : null;
|
|
7545
|
+
return accountIdMemo;
|
|
7546
|
+
}
|
|
7547
|
+
async function maybeRememberDriveKey(credentials, masterRaw) {
|
|
7548
|
+
if (!process.stdin.isTTY)
|
|
7549
|
+
return;
|
|
7550
|
+
const accountId = await driveAccountId(credentials);
|
|
7551
|
+
const existing = accountId ? readDriveKeyFile(accountId) : null;
|
|
7552
|
+
if (accountId && existing && existing.apiBase === credentials.apiBase && existing.masterKey)
|
|
7553
|
+
return;
|
|
7554
|
+
const answer = await askVisible("记住云盘钥匙到本机?下次免输口令(storage key --forget 清除)[y/N] ");
|
|
7555
|
+
if (answer.trim().toLowerCase() === "y") {
|
|
7556
|
+
await storeDriveKey(accountId, credentials.apiBase, masterRaw);
|
|
7557
|
+
console.log("✓ 已记住云盘钥匙到本机(storage key --forget 可清除)。");
|
|
7558
|
+
}
|
|
7559
|
+
}
|
|
7560
|
+
async function storageKeyCmd() {
|
|
7561
|
+
const credentials = requireCredentials();
|
|
7562
|
+
if (typeof credentials === "number")
|
|
7563
|
+
return credentials;
|
|
7564
|
+
const accountId = await driveAccountId(credentials);
|
|
7565
|
+
if (!accountId) {
|
|
7566
|
+
console.error("获取账号身份失败(凭证无效?)。");
|
|
7567
|
+
return EXIT_AUTH;
|
|
7568
|
+
}
|
|
7569
|
+
if (hasFlag("--forget")) {
|
|
7570
|
+
const had = readDriveKeyFile(accountId)?.masterKey !== undefined;
|
|
7571
|
+
forgetDriveKey(accountId);
|
|
7572
|
+
console.log(had ? "✓ 已清除本机记住的云盘钥匙,下次操作恢复口令输入。" : "本机没有记住的云盘钥匙。");
|
|
7573
|
+
return 0;
|
|
7574
|
+
}
|
|
7575
|
+
if (hasFlag("--remember")) {
|
|
7576
|
+
const existing = readDriveKeyFile(accountId);
|
|
7577
|
+
if (existing && existing.apiBase === credentials.apiBase && existing.masterKey) {
|
|
7578
|
+
console.log(`本机已记住云盘钥匙:${driveKeyPath(accountId)}`);
|
|
7579
|
+
return 0;
|
|
7580
|
+
}
|
|
7581
|
+
const raw = await driveMasterKeyRaw(credentials, { skipRememberPrompt: true });
|
|
7582
|
+
if (typeof raw === "number")
|
|
7583
|
+
return raw;
|
|
7584
|
+
await storeDriveKey(accountId, credentials.apiBase, raw);
|
|
7585
|
+
console.log(`✓ 已记住云盘钥匙到本机:${driveKeyPath(accountId)}(storage key --forget 清除)`);
|
|
7586
|
+
return 0;
|
|
7587
|
+
}
|
|
7588
|
+
const file = readDriveKeyFile(accountId);
|
|
7589
|
+
if (file && file.apiBase === credentials.apiBase && file.masterKey) {
|
|
7590
|
+
const when = typeof file.storedAt === "number" ? new Date(file.storedAt).toLocaleString() : "未知时间";
|
|
7591
|
+
console.log(`✓ 本机已记住云盘钥匙:${driveKeyPath(accountId)}(记住于 ${when})`);
|
|
7592
|
+
console.log(" 之后 storage 命令免输口令;Web 端轮换口令/恢复码后本钥匙自动失效。");
|
|
7593
|
+
console.log(" storage key --forget 清除;storage key --remember 重新解锁并记住。");
|
|
7594
|
+
} else {
|
|
7595
|
+
console.log("本机未记住云盘钥匙(每次操作需输入口令)。");
|
|
7596
|
+
console.log(" storage key --remember:解锁一次并记住;storage key --forget:清除记录。");
|
|
7597
|
+
}
|
|
7598
|
+
return 0;
|
|
7599
|
+
}
|
|
7600
|
+
var masterKeyRawMemo = null;
|
|
7601
|
+
async function driveMasterKeyRaw(credentials, opts = {}) {
|
|
7602
|
+
if (masterKeyRawMemo)
|
|
7603
|
+
return masterKeyRawMemo;
|
|
7604
|
+
const accountId = await driveAccountId(credentials);
|
|
7605
|
+
if (accountId) {
|
|
7606
|
+
const stored = await loadVerifiedDriveKey(credentials, accountId);
|
|
7607
|
+
if (stored) {
|
|
7608
|
+
const res = await api2(credentials.apiBase, "/v1/storage/keys", { headers: authHeaders(credentials) });
|
|
7609
|
+
if (res.status === 200) {
|
|
7610
|
+
masterKeyRawMemo = stored;
|
|
7611
|
+
return stored;
|
|
7612
|
+
}
|
|
7613
|
+
if (res.status === 404) {
|
|
7614
|
+
forgetDriveKey(accountId);
|
|
7615
|
+
} else {
|
|
7616
|
+
console.error(errorMessage(res.body, "获取云盘密钥失败。"));
|
|
7617
|
+
return exitCodeForApiError(res.body);
|
|
7618
|
+
}
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7621
|
+
const passphrase = await drivePassphrase();
|
|
7410
7622
|
if (!passphrase) {
|
|
7411
|
-
console.error("需要云盘口令:--passphrase <口令> 或环境变量 CRAB_PASSPHRASE
|
|
7623
|
+
console.error("需要云盘口令:--passphrase <口令> 或环境变量 CRAB_PASSPHRASE(交互终端会提示输入)。");
|
|
7412
7624
|
return EXIT_USAGE;
|
|
7413
7625
|
}
|
|
7414
7626
|
const res = await api2(credentials.apiBase, "/v1/storage/keys", { headers: authHeaders(credentials) });
|
|
@@ -7437,7 +7649,10 @@ async function driveMasterKey(credentials) {
|
|
|
7437
7649
|
}
|
|
7438
7650
|
console.log(`★ 恢复码:${recoveryCode}`);
|
|
7439
7651
|
console.log(" (口令遗忘时的唯一找回方式;平台无后门,丢失即数据不可解)");
|
|
7440
|
-
|
|
7652
|
+
if (!opts.skipRememberPrompt)
|
|
7653
|
+
await maybeRememberDriveKey(credentials, masterRaw);
|
|
7654
|
+
masterKeyRawMemo = masterRaw;
|
|
7655
|
+
return masterRaw;
|
|
7441
7656
|
}
|
|
7442
7657
|
if (res.status !== 200) {
|
|
7443
7658
|
console.error(errorMessage(res.body, "获取云盘密钥失败。"));
|
|
@@ -7446,12 +7661,22 @@ async function driveMasterKey(credentials) {
|
|
|
7446
7661
|
const keys = res.body.data;
|
|
7447
7662
|
try {
|
|
7448
7663
|
const kek = await importAesKey(await deriveKekBits(passphrase, b64urlDecode(keys.kdfSalt), keys.kdfIterations));
|
|
7449
|
-
|
|
7664
|
+
const masterRaw = await unwrapKeyBytes(kek, sealFromB64url(keys.wrappedMasterKey.slice(3)), MASTER_AAD);
|
|
7665
|
+
if (!opts.skipRememberPrompt)
|
|
7666
|
+
await maybeRememberDriveKey(credentials, masterRaw);
|
|
7667
|
+
masterKeyRawMemo = masterRaw;
|
|
7668
|
+
return masterRaw;
|
|
7450
7669
|
} catch {
|
|
7451
7670
|
console.error("口令错误。");
|
|
7452
7671
|
return 1;
|
|
7453
7672
|
}
|
|
7454
7673
|
}
|
|
7674
|
+
async function driveMasterKey(credentials) {
|
|
7675
|
+
const raw = await driveMasterKeyRaw(credentials);
|
|
7676
|
+
if (typeof raw === "number")
|
|
7677
|
+
return raw;
|
|
7678
|
+
return importAesKey(raw);
|
|
7679
|
+
}
|
|
7455
7680
|
async function storageUploadOne(credentials, filePath) {
|
|
7456
7681
|
let buf;
|
|
7457
7682
|
try {
|
|
@@ -7595,11 +7820,6 @@ async function storageDownload(token) {
|
|
|
7595
7820
|
let bytes = new Uint8Array(await res.arrayBuffer());
|
|
7596
7821
|
const wrapped = res.headers.get("x-drive-wrapped-key");
|
|
7597
7822
|
if (wrapped?.startsWith("u1.")) {
|
|
7598
|
-
const passphrase = drivePassphrase();
|
|
7599
|
-
if (!passphrase) {
|
|
7600
|
-
console.error("该文件已加密:--passphrase <口令> 或 CRAB_PASSPHRASE 提供云盘口令后重试。");
|
|
7601
|
-
return EXIT_USAGE;
|
|
7602
|
-
}
|
|
7603
7823
|
const master = await driveMasterKey(credentials);
|
|
7604
7824
|
if (typeof master === "number")
|
|
7605
7825
|
return master;
|
|
@@ -7666,13 +7886,20 @@ async function storageTag(token) {
|
|
|
7666
7886
|
}
|
|
7667
7887
|
async function storageCategory(token, value) {
|
|
7668
7888
|
if (!token || !value) {
|
|
7669
|
-
console.error("用法:crab storage category <id|文件名>
|
|
7889
|
+
console.error("用法:crab storage category <id|文件名> <分类id|分类名|none>(分类见 storage categories)");
|
|
7670
7890
|
return EXIT_USAGE;
|
|
7671
7891
|
}
|
|
7672
7892
|
const credentials = requireCredentials();
|
|
7673
7893
|
if (typeof credentials === "number")
|
|
7674
7894
|
return credentials;
|
|
7675
|
-
|
|
7895
|
+
let categoryId = null;
|
|
7896
|
+
if (value !== "none") {
|
|
7897
|
+
const resolved = await resolveCategoryId(credentials, value);
|
|
7898
|
+
if (typeof resolved === "number")
|
|
7899
|
+
return resolved;
|
|
7900
|
+
categoryId = resolved;
|
|
7901
|
+
}
|
|
7902
|
+
const result = await storagePatch(credentials, token, { categoryId });
|
|
7676
7903
|
if (typeof result === "number")
|
|
7677
7904
|
return result;
|
|
7678
7905
|
console.log(`✓ 分类已更新:${result.filename}`);
|
|
@@ -7690,8 +7917,10 @@ async function storagePublish(token, publish) {
|
|
|
7690
7917
|
if (typeof result === "number")
|
|
7691
7918
|
return result;
|
|
7692
7919
|
console.log(`✓ ${publish ? "已公开" : "已收回公开"}:${result.filename}`);
|
|
7693
|
-
if (publish)
|
|
7694
|
-
|
|
7920
|
+
if (publish) {
|
|
7921
|
+
const base = credentials.apiBase === DEFAULT_API ? undefined : credentials.apiBase;
|
|
7922
|
+
console.log(` 公开链接:${storagePublicUrl(result.id, base)}`);
|
|
7923
|
+
}
|
|
7695
7924
|
return 0;
|
|
7696
7925
|
}
|
|
7697
7926
|
async function storageMove(action, token) {
|
|
@@ -7856,28 +8085,116 @@ async function storageCategories() {
|
|
|
7856
8085
|
return 0;
|
|
7857
8086
|
}
|
|
7858
8087
|
for (const row of rows)
|
|
7859
|
-
console.log(`${row.id} ${row.name} (${row.fileCount})`);
|
|
8088
|
+
console.log(`${row.id} ${row.name} [${row.color ?? "—"}] (${row.fileCount})`);
|
|
7860
8089
|
return 0;
|
|
7861
8090
|
}
|
|
7862
|
-
async function
|
|
8091
|
+
async function resolveCategoryId(credentials, idOrName) {
|
|
8092
|
+
const res = await api2(credentials.apiBase, "/v1/storage/categories", { headers: authHeaders(credentials) });
|
|
8093
|
+
if (res.status !== 200) {
|
|
8094
|
+
console.error(errorMessage(res.body, "获取分类失败。"));
|
|
8095
|
+
return exitCodeForApiError(res.body);
|
|
8096
|
+
}
|
|
8097
|
+
const rows = res.body.data.categories;
|
|
8098
|
+
const byId = rows.find((row) => row.id === idOrName);
|
|
8099
|
+
if (byId)
|
|
8100
|
+
return byId.id;
|
|
8101
|
+
const byName = rows.filter((row) => row.name === idOrName);
|
|
8102
|
+
if (byName.length === 1)
|
|
8103
|
+
return byName[0].id;
|
|
8104
|
+
if (byName.length === 0) {
|
|
8105
|
+
console.error(`分类不存在:${idOrName}(crab storage categories 查看列表)`);
|
|
8106
|
+
return EXIT_NOT_FOUND;
|
|
8107
|
+
}
|
|
8108
|
+
console.error(`分类名重复:${idOrName},请用 id 指认。`);
|
|
8109
|
+
return EXIT_USAGE;
|
|
8110
|
+
}
|
|
8111
|
+
function positionalArgs(rest, valuedFlags) {
|
|
8112
|
+
const out = [];
|
|
8113
|
+
for (let i = 0;i < rest.length; i++) {
|
|
8114
|
+
if (valuedFlags.includes(rest[i])) {
|
|
8115
|
+
i++;
|
|
8116
|
+
continue;
|
|
8117
|
+
}
|
|
8118
|
+
if (rest[i].startsWith("--"))
|
|
8119
|
+
continue;
|
|
8120
|
+
out.push(rest[i]);
|
|
8121
|
+
}
|
|
8122
|
+
return out;
|
|
8123
|
+
}
|
|
8124
|
+
async function storageMkCat(rest) {
|
|
8125
|
+
const name = positionalArgs(rest, ["--color"]).join(" ") || undefined;
|
|
7863
8126
|
if (!name) {
|
|
7864
|
-
console.error("用法:crab storage mkcat <名称>");
|
|
8127
|
+
console.error("用法:crab storage mkcat <名称> [--color 色]");
|
|
7865
8128
|
return EXIT_USAGE;
|
|
7866
8129
|
}
|
|
7867
8130
|
const credentials = requireCredentials();
|
|
7868
8131
|
if (typeof credentials === "number")
|
|
7869
8132
|
return credentials;
|
|
8133
|
+
const color = readFlag("--color");
|
|
7870
8134
|
const res = await api2(credentials.apiBase, "/v1/storage/categories", {
|
|
7871
8135
|
method: "POST",
|
|
7872
8136
|
headers: authHeaders(credentials),
|
|
7873
|
-
body: JSON.stringify({ name })
|
|
8137
|
+
body: JSON.stringify({ name, ...color ? { color } : {} })
|
|
7874
8138
|
});
|
|
7875
8139
|
if (res.status !== 200 && res.status !== 201) {
|
|
7876
8140
|
console.error(errorMessage(res.body, "创建分类失败。"));
|
|
7877
8141
|
return exitCodeForApiError(res.body);
|
|
7878
8142
|
}
|
|
7879
8143
|
const row = res.body.data;
|
|
7880
|
-
console.log(`✓ 已创建分类:${row.name}(id ${row.id})`);
|
|
8144
|
+
console.log(`✓ 已创建分类:${row.name}(id ${row.id},颜色 ${row.color ?? "—"})`);
|
|
8145
|
+
return 0;
|
|
8146
|
+
}
|
|
8147
|
+
async function storageRenCat(target, rest) {
|
|
8148
|
+
const name = positionalArgs(rest, ["--color"]).join(" ") || undefined;
|
|
8149
|
+
if (!target || !name && !readFlag("--color")) {
|
|
8150
|
+
console.error("用法:crab storage rencat <id|分类名> <新名称> [--color 色]");
|
|
8151
|
+
return EXIT_USAGE;
|
|
8152
|
+
}
|
|
8153
|
+
const credentials = requireCredentials();
|
|
8154
|
+
if (typeof credentials === "number")
|
|
8155
|
+
return credentials;
|
|
8156
|
+
const id = await resolveCategoryId(credentials, target);
|
|
8157
|
+
if (typeof id === "number")
|
|
8158
|
+
return id;
|
|
8159
|
+
const body = {};
|
|
8160
|
+
if (name)
|
|
8161
|
+
body.name = name;
|
|
8162
|
+
const color = readFlag("--color");
|
|
8163
|
+
if (color)
|
|
8164
|
+
body.color = color;
|
|
8165
|
+
const res = await api2(credentials.apiBase, `/v1/storage/categories/${encodeURIComponent(id)}`, {
|
|
8166
|
+
method: "PATCH",
|
|
8167
|
+
headers: authHeaders(credentials),
|
|
8168
|
+
body: JSON.stringify(body)
|
|
8169
|
+
});
|
|
8170
|
+
if (res.status !== 200) {
|
|
8171
|
+
console.error(errorMessage(res.body, "更新分类失败。"));
|
|
8172
|
+
return exitCodeForApiError(res.body);
|
|
8173
|
+
}
|
|
8174
|
+
const row = res.body.data;
|
|
8175
|
+
console.log(`✓ 分类已更新:${row.name}(颜色 ${row.color ?? "—"})`);
|
|
8176
|
+
return 0;
|
|
8177
|
+
}
|
|
8178
|
+
async function storageRmCat(target) {
|
|
8179
|
+
if (!target) {
|
|
8180
|
+
console.error("用法:crab storage rmcat <id|分类名>(分类下文件回落未分类,不删文件)");
|
|
8181
|
+
return EXIT_USAGE;
|
|
8182
|
+
}
|
|
8183
|
+
const credentials = requireCredentials();
|
|
8184
|
+
if (typeof credentials === "number")
|
|
8185
|
+
return credentials;
|
|
8186
|
+
const id = await resolveCategoryId(credentials, target);
|
|
8187
|
+
if (typeof id === "number")
|
|
8188
|
+
return id;
|
|
8189
|
+
const res = await api2(credentials.apiBase, `/v1/storage/categories/${encodeURIComponent(id)}`, {
|
|
8190
|
+
method: "DELETE",
|
|
8191
|
+
headers: authHeaders(credentials)
|
|
8192
|
+
});
|
|
8193
|
+
if (res.status !== 200) {
|
|
8194
|
+
console.error(errorMessage(res.body, "删除分类失败。"));
|
|
8195
|
+
return exitCodeForApiError(res.body);
|
|
8196
|
+
}
|
|
8197
|
+
console.log("✓ 分类已删除,其中文件回落未分类。");
|
|
7881
8198
|
return 0;
|
|
7882
8199
|
}
|
|
7883
8200
|
async function storageTags() {
|
|
@@ -7902,6 +8219,487 @@ async function storageTags() {
|
|
|
7902
8219
|
console.log(`${row.tag} (${row.count})`);
|
|
7903
8220
|
return 0;
|
|
7904
8221
|
}
|
|
8222
|
+
var COLLAB_TASK_STATUSES = ["open", "done"];
|
|
8223
|
+
function collabSpaceToken(space) {
|
|
8224
|
+
return space.kind === "default" ? "default" : `space:${space.name}`;
|
|
8225
|
+
}
|
|
8226
|
+
function printCollabTaskRow(task) {
|
|
8227
|
+
const flags = [task.status === "done" ? "已完成" : null, task.summary ? "有摘要" : null].filter(Boolean);
|
|
8228
|
+
console.log(`${task.id} ${task.title} 记录 ${task.recordCount} ${formatTime(task.latestRecordAt)}${flags.length > 0 ? ` [${flags.join(" · ")}]` : ""}`);
|
|
8229
|
+
}
|
|
8230
|
+
async function resolveCollabSpace(credentials, arg) {
|
|
8231
|
+
const res = await api2(credentials.apiBase, "/v1/collab/spaces", { headers: authHeaders(credentials) });
|
|
8232
|
+
if (res.status !== 200) {
|
|
8233
|
+
console.error(errorMessage(res.body, "获取协作空间失败。"));
|
|
8234
|
+
if (res.body?.error?.code === "scope_denied") {
|
|
8235
|
+
console.error("提示:协作命令需要 collab scope——运行 crab login --scopes collab --space default 重新授权。");
|
|
8236
|
+
}
|
|
8237
|
+
return exitCodeForApiError(res.body);
|
|
8238
|
+
}
|
|
8239
|
+
const spaces = res.body.data.spaces;
|
|
8240
|
+
if (spaces.length === 0) {
|
|
8241
|
+
console.error("你还没有可访问的协作空间。");
|
|
8242
|
+
return EXIT_NOT_FOUND;
|
|
8243
|
+
}
|
|
8244
|
+
if (arg === undefined || arg === "") {
|
|
8245
|
+
if (spaces.length === 1)
|
|
8246
|
+
return spaces[0];
|
|
8247
|
+
console.error("你所在的协作空间不止一个,请用 --space 指定(default / space:<名称> / 空间 id):");
|
|
8248
|
+
for (const s of spaces)
|
|
8249
|
+
console.error(` ${collabSpaceToken(s)} (任务 ${s.taskCount})`);
|
|
8250
|
+
return EXIT_USAGE;
|
|
8251
|
+
}
|
|
8252
|
+
if (arg === "default") {
|
|
8253
|
+
const hit = spaces.find((s) => s.kind === "default");
|
|
8254
|
+
if (hit)
|
|
8255
|
+
return hit;
|
|
8256
|
+
console.error("没有找到你的默认协作空间。");
|
|
8257
|
+
return EXIT_NOT_FOUND;
|
|
8258
|
+
}
|
|
8259
|
+
if (arg.startsWith("space:")) {
|
|
8260
|
+
const name = arg.slice("space:".length).trim();
|
|
8261
|
+
if (!name) {
|
|
8262
|
+
console.error("space: 后面要跟空间名称(crab collab spaces 查看)。");
|
|
8263
|
+
return EXIT_USAGE;
|
|
8264
|
+
}
|
|
8265
|
+
const hit = spaces.filter((s) => s.name === name && s.status === "active");
|
|
8266
|
+
if (hit.length === 1)
|
|
8267
|
+
return hit[0];
|
|
8268
|
+
console.error(hit.length === 0 ? `没有名为「${name}」的 active 协作空间。` : `「${name}」命中多个空间,请用空间 id 指定。`);
|
|
8269
|
+
return EXIT_NOT_FOUND;
|
|
8270
|
+
}
|
|
8271
|
+
const byId = spaces.find((s) => s.id === arg);
|
|
8272
|
+
if (byId)
|
|
8273
|
+
return byId;
|
|
8274
|
+
console.error(`空间标识无效:${arg}
|
|
8275
|
+
应为 default、space:<名称> 或空间 id(crab collab spaces 查看)。`);
|
|
8276
|
+
return EXIT_USAGE;
|
|
8277
|
+
}
|
|
8278
|
+
async function collabSpacesList() {
|
|
8279
|
+
const credentials = requireCredentials();
|
|
8280
|
+
if (typeof credentials === "number")
|
|
8281
|
+
return credentials;
|
|
8282
|
+
const res = await api2(credentials.apiBase, "/v1/collab/spaces", { headers: authHeaders(credentials) });
|
|
8283
|
+
if (res.status !== 200) {
|
|
8284
|
+
console.error(errorMessage(res.body, "获取协作空间失败。"));
|
|
8285
|
+
return exitCodeForApiError(res.body);
|
|
8286
|
+
}
|
|
8287
|
+
const data = res.body.data;
|
|
8288
|
+
if (hasFlag("--json")) {
|
|
8289
|
+
console.log(JSON.stringify(data, null, 2));
|
|
8290
|
+
return 0;
|
|
8291
|
+
}
|
|
8292
|
+
if (data.spaces.length === 0) {
|
|
8293
|
+
console.log("你还没有可访问的协作空间。");
|
|
8294
|
+
return 0;
|
|
8295
|
+
}
|
|
8296
|
+
for (const s of data.spaces) {
|
|
8297
|
+
const storage = s.kind === "independent" ? ` · 存储 ${formatBytesCli(s.storageUsedBytes)}/${formatBytesCli(s.storageAllocatedBytes)}` : "";
|
|
8298
|
+
console.log(`${collabSpaceToken(s)} ${s.status === "archived" ? "[已归档·只读] " : ""}任务 ${s.taskCount}/${s.limits.taskLimit} · 受邀成员 ${s.memberCount}/${s.limits.invitedMemberLimit}${storage}`);
|
|
8299
|
+
console.log(` id ${s.id}`);
|
|
8300
|
+
}
|
|
8301
|
+
console.log(`独立空间名额:${data.independentUsed}/${data.independentLimit}(创建消耗 5 积分:crab collab space create <名称>)`);
|
|
8302
|
+
return 0;
|
|
8303
|
+
}
|
|
8304
|
+
async function collabSpaceCreate(name) {
|
|
8305
|
+
if (!name) {
|
|
8306
|
+
console.error("用法:crab collab space create <名称> [--allocation-mb <兆字节>] [--json]");
|
|
8307
|
+
return EXIT_USAGE;
|
|
8308
|
+
}
|
|
8309
|
+
const credentials = requireCredentials();
|
|
8310
|
+
if (typeof credentials === "number")
|
|
8311
|
+
return credentials;
|
|
8312
|
+
const allocationMb = Number.parseInt(readFlag("--allocation-mb") ?? "", 10);
|
|
8313
|
+
const body = { name };
|
|
8314
|
+
if (Number.isFinite(allocationMb))
|
|
8315
|
+
body.allocationBytes = allocationMb * 1024 * 1024;
|
|
8316
|
+
const res = await api2(credentials.apiBase, "/v1/collab/spaces", {
|
|
8317
|
+
method: "POST",
|
|
8318
|
+
headers: authHeaders(credentials),
|
|
8319
|
+
body: JSON.stringify(body)
|
|
8320
|
+
});
|
|
8321
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
8322
|
+
console.error(errorMessage(res.body, "创建失败。"));
|
|
8323
|
+
return exitCodeForApiError(res.body);
|
|
8324
|
+
}
|
|
8325
|
+
const data = res.body.data;
|
|
8326
|
+
if (hasFlag("--json")) {
|
|
8327
|
+
console.log(JSON.stringify(data, null, 2));
|
|
8328
|
+
return 0;
|
|
8329
|
+
}
|
|
8330
|
+
console.log(`✓ 独立协作空间已创建(id ${data.spaceId})`);
|
|
8331
|
+
console.log(` 消耗:${fmtUsd(data.chargedCredits)} · --space space:${name} 即可指向它`);
|
|
8332
|
+
return 0;
|
|
8333
|
+
}
|
|
8334
|
+
async function collabTasksList() {
|
|
8335
|
+
const credentials = requireCredentials();
|
|
8336
|
+
if (typeof credentials === "number")
|
|
8337
|
+
return credentials;
|
|
8338
|
+
const status = readFlag("--status");
|
|
8339
|
+
if (status && !COLLAB_TASK_STATUSES.includes(status)) {
|
|
8340
|
+
console.error(`未知状态:${status}
|
|
8341
|
+
可用:${COLLAB_TASK_STATUSES.join(", ")}`);
|
|
8342
|
+
return EXIT_USAGE;
|
|
8343
|
+
}
|
|
8344
|
+
const space = await resolveCollabSpace(credentials, readFlag("--space"));
|
|
8345
|
+
if (typeof space === "number")
|
|
8346
|
+
return space;
|
|
8347
|
+
const params = new URLSearchParams;
|
|
8348
|
+
if (status)
|
|
8349
|
+
params.set("status", status);
|
|
8350
|
+
const q = readFlag("--q");
|
|
8351
|
+
if (q)
|
|
8352
|
+
params.set("q", q);
|
|
8353
|
+
const limit = readFlag("--limit");
|
|
8354
|
+
if (limit)
|
|
8355
|
+
params.set("limit", limit);
|
|
8356
|
+
const cursor = readFlag("--cursor");
|
|
8357
|
+
if (cursor)
|
|
8358
|
+
params.set("cursor", cursor);
|
|
8359
|
+
const res = await api2(credentials.apiBase, `/v1/collab/spaces/${encodeURIComponent(space.id)}/tasks?${params}`, {
|
|
8360
|
+
headers: authHeaders(credentials)
|
|
8361
|
+
});
|
|
8362
|
+
if (res.status !== 200) {
|
|
8363
|
+
console.error(errorMessage(res.body, "获取任务列表失败。"));
|
|
8364
|
+
return exitCodeForApiError(res.body);
|
|
8365
|
+
}
|
|
8366
|
+
const page = res.body.data;
|
|
8367
|
+
if (hasFlag("--json")) {
|
|
8368
|
+
console.log(JSON.stringify({ ...page, space: { id: space.id, kind: space.kind, name: space.name } }, null, 2));
|
|
8369
|
+
return 0;
|
|
8370
|
+
}
|
|
8371
|
+
console.log(`空间:${collabSpaceToken(space)}${space.status === "archived" ? "(已归档·只读)" : ""}`);
|
|
8372
|
+
if (page.tasks.length === 0) {
|
|
8373
|
+
console.log('没有匹配的任务(crab collab task create --title "…" 新建)。');
|
|
8374
|
+
return 0;
|
|
8375
|
+
}
|
|
8376
|
+
for (const task of page.tasks)
|
|
8377
|
+
printCollabTaskRow(task);
|
|
8378
|
+
if (page.nextCursor)
|
|
8379
|
+
console.log(`(还有更多:--cursor ${page.nextCursor})`);
|
|
8380
|
+
return 0;
|
|
8381
|
+
}
|
|
8382
|
+
async function collabTaskDetail(id) {
|
|
8383
|
+
if (!id) {
|
|
8384
|
+
console.error("用法:crab collab task <id>(id 见 crab collab tasks)");
|
|
8385
|
+
return EXIT_USAGE;
|
|
8386
|
+
}
|
|
8387
|
+
const credentials = requireCredentials();
|
|
8388
|
+
if (typeof credentials === "number")
|
|
8389
|
+
return credentials;
|
|
8390
|
+
const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(id)}`, { headers: authHeaders(credentials) });
|
|
8391
|
+
if (res.status !== 200) {
|
|
8392
|
+
console.error(errorMessage(res.body, "获取任务失败。"));
|
|
8393
|
+
return exitCodeForApiError(res.body);
|
|
8394
|
+
}
|
|
8395
|
+
const task = res.body.data.task;
|
|
8396
|
+
if (hasFlag("--json")) {
|
|
8397
|
+
console.log(JSON.stringify(task, null, 2));
|
|
8398
|
+
return 0;
|
|
8399
|
+
}
|
|
8400
|
+
console.log(`${task.title} [${task.status === "done" ? "已完成" : "进行中"}]`);
|
|
8401
|
+
console.log(` id: ${task.id}`);
|
|
8402
|
+
console.log(` 记录 ${task.recordCount} 条 · 最新 ${formatTime(task.latestRecordAt)} · 创建 ${formatTime(task.createdAt)}`);
|
|
8403
|
+
if (task.summary)
|
|
8404
|
+
console.log(` 摘要: ${task.summary}`);
|
|
8405
|
+
console.log(" 记录流:crab collab task records " + task.id);
|
|
8406
|
+
return 0;
|
|
8407
|
+
}
|
|
8408
|
+
async function collabTaskCreate() {
|
|
8409
|
+
const title = readFlag("--title");
|
|
8410
|
+
const spaceArg = readFlag("--space");
|
|
8411
|
+
const bodyFlag = readFlag("--body");
|
|
8412
|
+
const bodyFile = readFlag("--body-file");
|
|
8413
|
+
if (!title) {
|
|
8414
|
+
console.error('用法:crab collab task create --space <arg> --title "..." [--body "..." | --body-file <path>] [--attach <fileId,...>] [--link <url,...>] [--json]');
|
|
8415
|
+
return EXIT_USAGE;
|
|
8416
|
+
}
|
|
8417
|
+
if (bodyFlag && bodyFile) {
|
|
8418
|
+
console.error("--body 与 --body-file 二选一。");
|
|
8419
|
+
return EXIT_USAGE;
|
|
8420
|
+
}
|
|
8421
|
+
let body = bodyFlag ?? "";
|
|
8422
|
+
if (bodyFile) {
|
|
8423
|
+
try {
|
|
8424
|
+
body = readFileSync2(bodyFile, "utf8");
|
|
8425
|
+
} catch {
|
|
8426
|
+
console.error(`读取正文文件失败:${bodyFile}`);
|
|
8427
|
+
return EXIT_USAGE;
|
|
8428
|
+
}
|
|
8429
|
+
}
|
|
8430
|
+
const credentials = requireCredentials();
|
|
8431
|
+
if (typeof credentials === "number")
|
|
8432
|
+
return credentials;
|
|
8433
|
+
const space = await resolveCollabSpace(credentials, spaceArg);
|
|
8434
|
+
if (typeof space === "number")
|
|
8435
|
+
return space;
|
|
8436
|
+
const fileIds = (readFlag("--attach") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
8437
|
+
const links = (readFlag("--link") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
8438
|
+
const res = await api2(credentials.apiBase, `/v1/collab/spaces/${encodeURIComponent(space.id)}/tasks`, {
|
|
8439
|
+
method: "POST",
|
|
8440
|
+
headers: authHeaders(credentials),
|
|
8441
|
+
body: JSON.stringify({ title, body, ...fileIds.length > 0 ? { fileIds } : {}, ...links.length > 0 ? { links } : {} })
|
|
8442
|
+
});
|
|
8443
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
8444
|
+
console.error(errorMessage(res.body, "创建任务失败。"));
|
|
8445
|
+
return exitCodeForApiError(res.body);
|
|
8446
|
+
}
|
|
8447
|
+
const data = res.body.data;
|
|
8448
|
+
if (hasFlag("--json")) {
|
|
8449
|
+
console.log(JSON.stringify(data, null, 2));
|
|
8450
|
+
return 0;
|
|
8451
|
+
}
|
|
8452
|
+
console.log(`✓ 任务已创建(id ${data.taskId})`);
|
|
8453
|
+
console.log(` 首条记录已入流;追加记录:crab post ${data.taskId} --body "..."`);
|
|
8454
|
+
return 0;
|
|
8455
|
+
}
|
|
8456
|
+
async function collabTaskRecords(id) {
|
|
8457
|
+
if (!id) {
|
|
8458
|
+
console.error("用法:crab collab task records <id> [--limit N] [--cursor <cursor>] [--json]");
|
|
8459
|
+
return EXIT_USAGE;
|
|
8460
|
+
}
|
|
8461
|
+
const credentials = requireCredentials();
|
|
8462
|
+
if (typeof credentials === "number")
|
|
8463
|
+
return credentials;
|
|
8464
|
+
const params = new URLSearchParams;
|
|
8465
|
+
const limit = readFlag("--limit");
|
|
8466
|
+
if (limit)
|
|
8467
|
+
params.set("limit", limit);
|
|
8468
|
+
const cursor = readFlag("--cursor");
|
|
8469
|
+
if (cursor)
|
|
8470
|
+
params.set("cursor", cursor);
|
|
8471
|
+
const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(id)}/records?${params}`, {
|
|
8472
|
+
headers: authHeaders(credentials)
|
|
8473
|
+
});
|
|
8474
|
+
if (res.status !== 200) {
|
|
8475
|
+
console.error(errorMessage(res.body, "读取记录流失败。"));
|
|
8476
|
+
return exitCodeForApiError(res.body);
|
|
8477
|
+
}
|
|
8478
|
+
const page = res.body.data;
|
|
8479
|
+
if (hasFlag("--json")) {
|
|
8480
|
+
console.log(JSON.stringify(page, null, 2));
|
|
8481
|
+
return 0;
|
|
8482
|
+
}
|
|
8483
|
+
if (page.records.length === 0) {
|
|
8484
|
+
console.log("该任务还没有记录。");
|
|
8485
|
+
return 0;
|
|
8486
|
+
}
|
|
8487
|
+
for (const record of page.records) {
|
|
8488
|
+
if (record.kind === "status_change") {
|
|
8489
|
+
console.log(`—— ${record.authorName ?? record.authorAccountId} ${record.actorType === "agent" ? "(经 Agent)" : ""}变更了任务状态 · ${formatTime(record.createdAt)} ——`);
|
|
8490
|
+
continue;
|
|
8491
|
+
}
|
|
8492
|
+
const via = record.via ? `(经 ${record.via.token})` : record.actorType === "agent" ? "(经 Agent)" : "";
|
|
8493
|
+
console.log(`【${record.authorName ?? record.authorAccountId}${via} · ${formatTime(record.createdAt)}】`);
|
|
8494
|
+
if (record.body)
|
|
8495
|
+
console.log(record.body);
|
|
8496
|
+
for (const ref of record.refs) {
|
|
8497
|
+
if (ref.kind === "link")
|
|
8498
|
+
console.log(` \uD83D\uDD17 ${ref.url}`);
|
|
8499
|
+
else
|
|
8500
|
+
console.log(` \uD83D\uDCCE ${ref.filename ?? ref.fileId}${ref.sizeBytes ? `(${formatBytesCli(ref.sizeBytes)}${ref.spaceOwned ? " · 空间文件" : " · 云盘引用"})` : ""}`);
|
|
8501
|
+
}
|
|
8502
|
+
console.log("");
|
|
8503
|
+
}
|
|
8504
|
+
if (page.nextCursor)
|
|
8505
|
+
console.log(`(还有更早的记录:--cursor ${page.nextCursor})`);
|
|
8506
|
+
return 0;
|
|
8507
|
+
}
|
|
8508
|
+
async function collabPushRecord(taskId) {
|
|
8509
|
+
if (!taskId) {
|
|
8510
|
+
console.error('用法:crab post <taskId> (--body "..." | --body-file <path>) [--attach <fileId,...>] [--link <url,...>] [--json]');
|
|
8511
|
+
return EXIT_USAGE;
|
|
8512
|
+
}
|
|
8513
|
+
const bodyFlag = readFlag("--body");
|
|
8514
|
+
const bodyFile = readFlag("--body-file");
|
|
8515
|
+
if (!bodyFlag && !bodyFile) {
|
|
8516
|
+
console.error('需要 --body "..." 或 --body-file <path>(记录正文必填,Markdown)。');
|
|
8517
|
+
return EXIT_USAGE;
|
|
8518
|
+
}
|
|
8519
|
+
if (bodyFlag && bodyFile) {
|
|
8520
|
+
console.error("--body 与 --body-file 二选一。");
|
|
8521
|
+
return EXIT_USAGE;
|
|
8522
|
+
}
|
|
8523
|
+
let body = bodyFlag ?? "";
|
|
8524
|
+
if (bodyFile) {
|
|
8525
|
+
try {
|
|
8526
|
+
body = readFileSync2(bodyFile, "utf8");
|
|
8527
|
+
} catch {
|
|
8528
|
+
console.error(`读取正文文件失败:${bodyFile}`);
|
|
8529
|
+
return EXIT_USAGE;
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
if (!body.trim()) {
|
|
8533
|
+
console.error("记录正文不能为空。");
|
|
8534
|
+
return EXIT_USAGE;
|
|
8535
|
+
}
|
|
8536
|
+
const credentials = requireCredentials();
|
|
8537
|
+
if (typeof credentials === "number")
|
|
8538
|
+
return credentials;
|
|
8539
|
+
const fileIds = (readFlag("--attach") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
8540
|
+
const links = (readFlag("--link") ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
8541
|
+
const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(taskId)}/records`, {
|
|
8542
|
+
method: "POST",
|
|
8543
|
+
headers: authHeaders(credentials),
|
|
8544
|
+
body: JSON.stringify({ body, ...fileIds.length > 0 ? { fileIds } : {}, ...links.length > 0 ? { links } : {} })
|
|
8545
|
+
});
|
|
8546
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
8547
|
+
console.error(errorMessage(res.body, "推送失败。"));
|
|
8548
|
+
return exitCodeForApiError(res.body);
|
|
8549
|
+
}
|
|
8550
|
+
const data = res.body.data;
|
|
8551
|
+
if (hasFlag("--json")) {
|
|
8552
|
+
console.log(JSON.stringify(data, null, 2));
|
|
8553
|
+
return 0;
|
|
8554
|
+
}
|
|
8555
|
+
console.log(`✓ 记录已推送(id ${data.recordId})`);
|
|
8556
|
+
if (data.via)
|
|
8557
|
+
console.log(` 活动事件 evt: ${data.via.eventId}`);
|
|
8558
|
+
return 0;
|
|
8559
|
+
}
|
|
8560
|
+
async function collabTaskStatus(action, id) {
|
|
8561
|
+
if (!id) {
|
|
8562
|
+
console.error(`用法:crab collab task ${action} <id>`);
|
|
8563
|
+
return EXIT_USAGE;
|
|
8564
|
+
}
|
|
8565
|
+
const credentials = requireCredentials();
|
|
8566
|
+
if (typeof credentials === "number")
|
|
8567
|
+
return credentials;
|
|
8568
|
+
const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(id)}/${action}`, {
|
|
8569
|
+
method: "POST",
|
|
8570
|
+
headers: authHeaders(credentials)
|
|
8571
|
+
});
|
|
8572
|
+
if (res.status !== 200) {
|
|
8573
|
+
console.error(errorMessage(res.body, "状态更新失败。"));
|
|
8574
|
+
return exitCodeForApiError(res.body);
|
|
8575
|
+
}
|
|
8576
|
+
const task = res.body.data.task;
|
|
8577
|
+
console.log(`✓ ${action === "done" ? "已完成" : "已重开"}:${task.title}(状态变更已入记录流,记录 ${task.recordCount} 条)`);
|
|
8578
|
+
return 0;
|
|
8579
|
+
}
|
|
8580
|
+
async function collabTaskSummary(id) {
|
|
8581
|
+
if (!id) {
|
|
8582
|
+
console.error('用法:crab collab task summary <id> (--text "..." | --text-file <path> | --clear) [--json]');
|
|
8583
|
+
return EXIT_USAGE;
|
|
8584
|
+
}
|
|
8585
|
+
const text = readFlag("--text");
|
|
8586
|
+
const textFile = readFlag("--text-file");
|
|
8587
|
+
const clear = hasFlag("--clear");
|
|
8588
|
+
const sources = [text !== undefined, textFile !== undefined, clear].filter(Boolean).length;
|
|
8589
|
+
if (sources === 0) {
|
|
8590
|
+
console.error('需要 --text "..."(新摘要)、--text-file <path> 或 --clear(清空)之一。');
|
|
8591
|
+
return EXIT_USAGE;
|
|
8592
|
+
}
|
|
8593
|
+
if (sources > 1) {
|
|
8594
|
+
console.error("--text / --text-file / --clear 三选一。");
|
|
8595
|
+
return EXIT_USAGE;
|
|
8596
|
+
}
|
|
8597
|
+
let summary = text ?? "";
|
|
8598
|
+
if (textFile) {
|
|
8599
|
+
try {
|
|
8600
|
+
summary = readFileSync2(textFile, "utf8");
|
|
8601
|
+
} catch {
|
|
8602
|
+
console.error(`读取摘要文件失败:${textFile}`);
|
|
8603
|
+
return EXIT_USAGE;
|
|
8604
|
+
}
|
|
8605
|
+
}
|
|
8606
|
+
const credentials = requireCredentials();
|
|
8607
|
+
if (typeof credentials === "number")
|
|
8608
|
+
return credentials;
|
|
8609
|
+
const res = await api2(credentials.apiBase, `/v1/collab/tasks/${encodeURIComponent(id)}/summary`, {
|
|
8610
|
+
method: "PUT",
|
|
8611
|
+
headers: authHeaders(credentials),
|
|
8612
|
+
body: JSON.stringify({ summary })
|
|
8613
|
+
});
|
|
8614
|
+
if (res.status !== 200) {
|
|
8615
|
+
console.error(errorMessage(res.body, "摘要更新失败。"));
|
|
8616
|
+
return exitCodeForApiError(res.body);
|
|
8617
|
+
}
|
|
8618
|
+
const task = res.body.data.task;
|
|
8619
|
+
if (hasFlag("--json")) {
|
|
8620
|
+
console.log(JSON.stringify(task, null, 2));
|
|
8621
|
+
return 0;
|
|
8622
|
+
}
|
|
8623
|
+
console.log(task.summary ? `✓ 摘要已更新:${task.title}` : `✓ 摘要已清空:${task.title}`);
|
|
8624
|
+
if (task.summary)
|
|
8625
|
+
console.log(` ${task.summary.length > 200 ? `${task.summary.slice(0, 200)}…` : task.summary}`);
|
|
8626
|
+
return 0;
|
|
8627
|
+
}
|
|
8628
|
+
async function collabMembersList() {
|
|
8629
|
+
const credentials = requireCredentials();
|
|
8630
|
+
if (typeof credentials === "number")
|
|
8631
|
+
return credentials;
|
|
8632
|
+
const space = await resolveCollabSpace(credentials, readFlag("--space"));
|
|
8633
|
+
if (typeof space === "number")
|
|
8634
|
+
return space;
|
|
8635
|
+
const res = await api2(credentials.apiBase, `/v1/collab/spaces/${encodeURIComponent(space.id)}/members`, {
|
|
8636
|
+
headers: authHeaders(credentials)
|
|
8637
|
+
});
|
|
8638
|
+
if (res.status !== 200) {
|
|
8639
|
+
console.error(errorMessage(res.body, "获取成员失败。"));
|
|
8640
|
+
return exitCodeForApiError(res.body);
|
|
8641
|
+
}
|
|
8642
|
+
const members = res.body.data.members;
|
|
8643
|
+
if (hasFlag("--json")) {
|
|
8644
|
+
console.log(JSON.stringify(members, null, 2));
|
|
8645
|
+
return 0;
|
|
8646
|
+
}
|
|
8647
|
+
console.log(`空间:${collabSpaceToken(space)}`);
|
|
8648
|
+
for (const member of members) {
|
|
8649
|
+
console.log(`${member.username} ${member.displayName ?? ""} [${member.role === "owner" ? "创建者" : "成员"}] 加入 ${formatTime(member.joinedAt)}`);
|
|
8650
|
+
}
|
|
8651
|
+
return 0;
|
|
8652
|
+
}
|
|
8653
|
+
async function collabInvite(username) {
|
|
8654
|
+
if (!username) {
|
|
8655
|
+
console.error("用法:crab collab invite <username> [--space <arg>] [--json]");
|
|
8656
|
+
return EXIT_USAGE;
|
|
8657
|
+
}
|
|
8658
|
+
const credentials = requireCredentials();
|
|
8659
|
+
if (typeof credentials === "number")
|
|
8660
|
+
return credentials;
|
|
8661
|
+
const space = await resolveCollabSpace(credentials, readFlag("--space"));
|
|
8662
|
+
if (typeof space === "number")
|
|
8663
|
+
return space;
|
|
8664
|
+
const res = await api2(credentials.apiBase, `/v1/collab/spaces/${encodeURIComponent(space.id)}/members`, {
|
|
8665
|
+
method: "POST",
|
|
8666
|
+
headers: authHeaders(credentials),
|
|
8667
|
+
body: JSON.stringify({ username })
|
|
8668
|
+
});
|
|
8669
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
8670
|
+
console.error(errorMessage(res.body, "邀请失败。"));
|
|
8671
|
+
return exitCodeForApiError(res.body);
|
|
8672
|
+
}
|
|
8673
|
+
if (hasFlag("--json")) {
|
|
8674
|
+
console.log(JSON.stringify({ ok: true, spaceId: space.id, username }, null, 2));
|
|
8675
|
+
return 0;
|
|
8676
|
+
}
|
|
8677
|
+
console.log(`✓ 已邀请 ${username} 加入 ${collabSpaceToken(space)}。`);
|
|
8678
|
+
return 0;
|
|
8679
|
+
}
|
|
8680
|
+
async function collabActivity() {
|
|
8681
|
+
const credentials = requireCredentials();
|
|
8682
|
+
if (typeof credentials === "number")
|
|
8683
|
+
return credentials;
|
|
8684
|
+
const res = await api2(credentials.apiBase, "/v1/collab/agent-activity", { headers: authHeaders(credentials) });
|
|
8685
|
+
if (res.status !== 200) {
|
|
8686
|
+
console.error(errorMessage(res.body, "获取协作活动失败。"));
|
|
8687
|
+
return exitCodeForApiError(res.body);
|
|
8688
|
+
}
|
|
8689
|
+
const activity = res.body.data.activity;
|
|
8690
|
+
if (hasFlag("--json")) {
|
|
8691
|
+
console.log(JSON.stringify(activity, null, 2));
|
|
8692
|
+
return 0;
|
|
8693
|
+
}
|
|
8694
|
+
if (activity.length === 0) {
|
|
8695
|
+
console.log("还没有协作活动事件。");
|
|
8696
|
+
return 0;
|
|
8697
|
+
}
|
|
8698
|
+
for (const entry of activity) {
|
|
8699
|
+
console.log(`${formatTime(entry.createdAt)} ${entry.action.padEnd(15)} ${entry.actorType === "agent" ? "agent" : "user "} ${entry.actorLabel ?? "—"}${entry.taskId ? ` 任务 ${entry.taskId}` : ""}`);
|
|
8700
|
+
}
|
|
8701
|
+
return 0;
|
|
8702
|
+
}
|
|
7905
8703
|
async function init() {
|
|
7906
8704
|
if (loadCredentials()) {
|
|
7907
8705
|
console.log("已登录(crab whoami 查看身份)。");
|
|
@@ -8021,7 +8819,11 @@ switch (command) {
|
|
|
8021
8819
|
if (sub === "categories")
|
|
8022
8820
|
process.exit(await storageCategories());
|
|
8023
8821
|
if (sub === "mkcat")
|
|
8024
|
-
process.exit(await storageMkCat(args.slice(2)
|
|
8822
|
+
process.exit(await storageMkCat(args.slice(2)));
|
|
8823
|
+
if (sub === "rencat")
|
|
8824
|
+
process.exit(await storageRenCat(args[2], args.slice(3)));
|
|
8825
|
+
if (sub === "rmcat")
|
|
8826
|
+
process.exit(await storageRmCat(args[2]));
|
|
8025
8827
|
if (sub === "tags")
|
|
8026
8828
|
process.exit(await storageTags());
|
|
8027
8829
|
if (sub === "share")
|
|
@@ -8030,8 +8832,10 @@ switch (command) {
|
|
|
8030
8832
|
process.exit(await storageShares(args[2]));
|
|
8031
8833
|
if (sub === "unshare")
|
|
8032
8834
|
process.exit(await storageUnshare(args[2]));
|
|
8835
|
+
if (sub === "key")
|
|
8836
|
+
process.exit(await storageKeyCmd());
|
|
8033
8837
|
console.error(sub ? `未知子命令:storage ${sub}
|
|
8034
|
-
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | tags | share | shares | unshare
|
|
8838
|
+
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | rencat | rmcat | tags | share | shares | unshare | key
|
|
8035
8839
|
`);
|
|
8036
8840
|
console.log(usage);
|
|
8037
8841
|
process.exit(EXIT_USAGE);
|
|
@@ -8049,6 +8853,45 @@ switch (command) {
|
|
|
8049
8853
|
console.log(usage);
|
|
8050
8854
|
process.exit(EXIT_USAGE);
|
|
8051
8855
|
}
|
|
8856
|
+
case "collab": {
|
|
8857
|
+
const sub = args[1];
|
|
8858
|
+
if (sub === "spaces")
|
|
8859
|
+
process.exit(await collabSpacesList());
|
|
8860
|
+
if (sub === "space") {
|
|
8861
|
+
if (args[2] === "create")
|
|
8862
|
+
process.exit(await collabSpaceCreate(args.slice(3).find((x) => !x.startsWith("--"))));
|
|
8863
|
+
console.error(`用法:crab collab space create <名称> [--allocation-mb <兆字节>] [--json]
|
|
8864
|
+
`);
|
|
8865
|
+
process.exit(EXIT_USAGE);
|
|
8866
|
+
}
|
|
8867
|
+
if (sub === "tasks")
|
|
8868
|
+
process.exit(await collabTasksList());
|
|
8869
|
+
if (sub === "task") {
|
|
8870
|
+
const op = args[2];
|
|
8871
|
+
if (op === "create")
|
|
8872
|
+
process.exit(await collabTaskCreate());
|
|
8873
|
+
if (op === "records")
|
|
8874
|
+
process.exit(await collabTaskRecords(args[3]));
|
|
8875
|
+
if (op === "done" || op === "reopen")
|
|
8876
|
+
process.exit(await collabTaskStatus(op, args[3]));
|
|
8877
|
+
if (op === "summary")
|
|
8878
|
+
process.exit(await collabTaskSummary(args[3]));
|
|
8879
|
+
process.exit(await collabTaskDetail(op));
|
|
8880
|
+
}
|
|
8881
|
+
if (sub === "members")
|
|
8882
|
+
process.exit(await collabMembersList());
|
|
8883
|
+
if (sub === "invite")
|
|
8884
|
+
process.exit(await collabInvite(args[2]));
|
|
8885
|
+
if (sub === "activity")
|
|
8886
|
+
process.exit(await collabActivity());
|
|
8887
|
+
console.error(sub ? `未知子命令:collab ${sub}
|
|
8888
|
+
` : `缺少子命令:collab spaces | space create | tasks | task | members | invite | activity
|
|
8889
|
+
`);
|
|
8890
|
+
console.log(usage);
|
|
8891
|
+
process.exit(EXIT_USAGE);
|
|
8892
|
+
}
|
|
8893
|
+
case "post":
|
|
8894
|
+
process.exit(await collabPushRecord(args[1]));
|
|
8052
8895
|
case "logout":
|
|
8053
8896
|
process.exit(await logout());
|
|
8054
8897
|
case "version":
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crabcloud-collab
|
|
3
|
+
description: Work in the user's Crab Cloud collaboration spaces (协作空间) through the `crab` CLI — list spaces and members, create and complete tasks, push immutable execution records (Markdown + attachments/links), maintain the mutable task summary, and invite linked contacts. Use this skill when the user asks to create/update/check collaboration tasks or spaces on Crab Cloud ("建个任务", "协作空间里有什么", "把这个进度推送到任务", "邀请他进空间", "crab collab"), or wants an agent to record work progress into a task — even if they never say "crab" or "Crab Cloud" explicitly.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Crab Cloud 协作(crab collab)
|
|
7
|
+
|
|
8
|
+
协作是用户的任务协作应用:容器是协作空间(默认个人空间注册即有 + 独立付费
|
|
9
|
+
空间),原子单位是任务。任务 = 不可改的执行记录流(GitHub issue 式:什么节点
|
|
10
|
+
做了什么,人机同写),另有一个可随时覆盖的「摘要」字段表达当前态概览。本
|
|
11
|
+
skill 覆盖协作域的 agent 通道;账号/令牌管理见 `crabcloud` skill,邮箱见
|
|
12
|
+
`crabcloud-mail`,云盘见 `crabcloud-storage`。
|
|
13
|
+
|
|
14
|
+
## 第一步:确认可用
|
|
15
|
+
|
|
16
|
+
- 运行 `crab --version` 确认已安装。命令不存在时让用户运行
|
|
17
|
+
`npx @crabcloud/cli init`(登录 + 安装本 skill 一步完成);已装过但命令缺失
|
|
18
|
+
时运行 `npx @crabcloud/cli@latest init` 升级。
|
|
19
|
+
- CLI 基于 npx 分发,**本机没有 `crab` 命令时,下文所有 `crab <命令>` 都可以
|
|
20
|
+
`npx @crabcloud/cli <命令>` 等价执行**(例如 `npx @crabcloud/cli collab tasks`)。
|
|
21
|
+
- 未登录(退出码 3)引导用户 `crab login`。
|
|
22
|
+
- **协作命令需要空间绑定**:授权时必须带 `--space`(`default` = 默认空间,
|
|
23
|
+
`space:<名称>` = 按名唯一命中)。含 collab scope 但没带 `--space` 会直接报
|
|
24
|
+
用法错误(退出码 2)。正确示例:
|
|
25
|
+
`crab login --scopes collab --space default`
|
|
26
|
+
- 动作需要对应 scope:读(spaces/tasks/records/members)`collab.read`,写
|
|
27
|
+
(建任务/推送/完成/摘要/邀请)`collab.write`——一般整组授权 `--scopes collab`。
|
|
28
|
+
- 退出码 4 有两种协作专属情形,处置都是重新授权:
|
|
29
|
+
- `collab_space_not_bound`:令牌没绑空间——按上面带 `--space` 重新 `crab login`;
|
|
30
|
+
- `collab_space_mismatch`:令牌绑的是别的空间——换绑到目标空间(`--space`
|
|
31
|
+
指对)重新授权。**不要**反复重试同一令牌。
|
|
32
|
+
- 推送记录免费(不消耗积分);建独立空间(5 积分)与扩容才计费,见「纪律」。
|
|
33
|
+
|
|
34
|
+
## 空间怎么指(--space)
|
|
35
|
+
|
|
36
|
+
所有落到一个空间的命令都接受 `--space <arg>`,`<arg>` 三种形态:
|
|
37
|
+
|
|
38
|
+
1. `default` —— 用户的默认个人协作空间(注册即有);
|
|
39
|
+
2. `space:<名称>` —— 按名称唯一命中(active 空间);
|
|
40
|
+
3. 空间 id —— `crab collab spaces` 查到的 id。
|
|
41
|
+
|
|
42
|
+
**缺省 `--space` 时**:只在一个空间则自动采用;多个则 CLI 列出候选并要求显式
|
|
43
|
+
指定——不要瞎猜,先 `crab collab spaces` 看一眼再带上正确的 `--space`。
|
|
44
|
+
|
|
45
|
+
## 意图 → 命令映射
|
|
46
|
+
|
|
47
|
+
| 用户意图 | 命令 |
|
|
48
|
+
|---|---|
|
|
49
|
+
| 有哪些协作空间 | `crab collab spaces`(任务/成员计数、限额、独立空间名额) |
|
|
50
|
+
| 建独立协作空间 | `crab collab space create <名称> [--allocation-mb 500]`(**消耗 5 积分**,见纪律) |
|
|
51
|
+
| 空间里有什么任务 | `crab collab tasks [--space <arg>]`(倒序,含摘要/记录数;`--status open\|done`、`--q 关键字` 搜标题与记录正文) |
|
|
52
|
+
| 任务详情 | `crab collab task <id>`(状态/摘要/记录数) |
|
|
53
|
+
| 建任务 | `crab collab task create --space <arg> --title "..." --body "首条记录(Markdown)"`(也可 `--body-file <路径>`;附件 `--attach <fileId,...>`、链接 `--link <url,...>`) |
|
|
54
|
+
| 读任务的来龙去脉 | `crab collab task records <id>`(倒序记录流,Markdown 源文;`--cursor` 翻更早) |
|
|
55
|
+
| 推送工作记录 | `crab post <taskId> --body "做了什么(Markdown)"`(糖命令,等价 records 写入;免费) |
|
|
56
|
+
| 任务做完了 | `crab collab task done <id>`(入流一条状态变更);反悔 `crab collab task reopen <id>` |
|
|
57
|
+
| 维护任务摘要 | `crab collab task summary <id> --text "当前态概览"`(可随时覆盖;`--clear` 清空;不入记录流) |
|
|
58
|
+
| 谁在空间里 | `crab collab members [--space <arg>]` |
|
|
59
|
+
| 邀人进空间 | `crab collab invite <username> [--space <arg>]`(**只能邀联系人中已关联的平台用户**,见纪律) |
|
|
60
|
+
| 看协作活动 | `crab collab activity`(Agent 推送/读取、任务创建、摘要更新事件) |
|
|
61
|
+
|
|
62
|
+
任务 id 从 `crab collab tasks` 拿;`crab post` 的 `<taskId>` 同源。
|
|
63
|
+
|
|
64
|
+
## 机器可读输出
|
|
65
|
+
|
|
66
|
+
程序化消费**始终加 `--json`**:
|
|
67
|
+
|
|
68
|
+
- `collab spaces --json` → `{ spaces: [...], independentLimit, independentUsed }`,
|
|
69
|
+
条目含 `id / kind / name / status / taskCount / memberCount / limits`;
|
|
70
|
+
- `collab tasks --json` → `{ tasks: [...], nextCursor }`,条目含
|
|
71
|
+
`id / title / status / summary / recordCount / latestRecordAt`(游标分页);
|
|
72
|
+
- `collab task records --json` → `{ records: [...], nextCursor }`,条目含
|
|
73
|
+
`id / kind / body / authorName / via / refs / createdAt`;
|
|
74
|
+
- `post --json` → `{ recordId, via?: { token, eventId } }`(via.eventId 即该条
|
|
75
|
+
记录 id,可确定性回查);
|
|
76
|
+
- `collab task <id> --json` / `collab members --json` / `collab activity --json`
|
|
77
|
+
同构。
|
|
78
|
+
|
|
79
|
+
## 纪律
|
|
80
|
+
|
|
81
|
+
- **记录流不可改**:推上去就是历史,没有编辑/删除——推送前把内容写对;写错了
|
|
82
|
+
补一条更正记录,不要试图「撤回」。
|
|
83
|
+
- **摘要是可变当前态**:表达「现在进展到哪」,不是流水账;每次推送记录后若
|
|
84
|
+
摘要已过时,顺手 `crab collab task summary` 刷新,让不看全程的人一眼看懂。
|
|
85
|
+
- **建独立空间是付费动作(5 积分)**:先向用户复述名称与自分配存储(默认
|
|
86
|
+
500MB,占订阅总池),确认后再执行;`collab space create` 不要当作顺手操作。
|
|
87
|
+
- **邀请有前置**:目标必须是联系人簿里已关联的平台用户(`crab contacts add`
|
|
88
|
+
站内地址自动关联);报「只能邀请联系人中已关联的平台用户」时,先引导建联
|
|
89
|
+
系人,不要换名字硬试。
|
|
90
|
+
- **附件引用不复制**:`--attach` 的 fileId 是云盘/空间已有文件的引用(≤10 个),
|
|
91
|
+
只能引用空间成员的云盘文件或本空间直传文件;跨空间文件会被 403 拒绝。
|
|
92
|
+
- 单任务记录流上限 500 条(状态变更也占位):任务粒度别太粗,做完就 `done`,
|
|
93
|
+
新工作开新任务。
|
|
94
|
+
- 读记录流对 Agent 是一等审计事件(thread.read 入活动流)——正常读即可,但
|
|
95
|
+
不要在循环里无意义地反复拉全量记录。
|
|
@@ -1,14 +1,15 @@
|
|
|
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
|
|
|
@@ -29,22 +30,27 @@ description: Manage the user's Crab Cloud Drive (云盘 — unified storage for
|
|
|
29
30
|
|
|
30
31
|
| 用户意图 | 命令 |
|
|
31
32
|
|---|---|
|
|
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
|
|
33
|
+
| 云盘里有什么 | `crab storage ls`(按时间倒序,默认 30 条;`--limit` / `--offset` 翻页) |
|
|
34
|
+
| 找文件 | `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
35
|
| 看回收站 | `crab storage ls --trash` |
|
|
35
|
-
| 上传文件 | `crab storage upload
|
|
36
|
-
| 下载文件 | `crab storage download <id
|
|
36
|
+
| 上传文件 | `crab storage upload <路径...>`(多文件串行;直传优先,自动回退中转;默认端到端加密——口令三来源见下节,首次使用自动初始化并打印一次性恢复码) |
|
|
37
|
+
| 下载文件 | `crab storage download <id\|文件名\|分享链接> [-o 输出路径] [--passcode 提取码]`(默认按原文件名存当前目录;**凭完整分享链接可免登录下载并本地解密**——`#` 后是密钥片段,这是 agent→agent 的主通道;带提取码的分享加 `--passcode`) |
|
|
37
38
|
| 改名 | `crab storage rename <id\|文件名> <新名称>` |
|
|
38
39
|
| 加/删标签 | `crab storage tag <id\|文件名> --add a,b [--remove c,d]` |
|
|
39
|
-
| 归类
|
|
40
|
+
| 归类 | `crab storage category <id\|文件名> <分类id\|分类名\|none>`(分类名唯一命中即解析) |
|
|
41
|
+
| 建分类 | `crab storage mkcat <名称> [--color 色]` |
|
|
42
|
+
| 改分类名/颜色 | `crab storage rencat <id\|分类名> <新名称> [--color 色]` |
|
|
43
|
+
| 删分类 | `crab storage rmcat <id\|分类名>`(分类下文件回落未分类,不删文件) |
|
|
40
44
|
| 公开分享 | `crab storage publish <id\|文件名>`(打印公开直链;收回用 `unpublish`) |
|
|
41
45
|
| 可控分享(有效期/提取码/撤销) | `crab storage share <id\|文件名> [--expires 7\|30\|90\|forever] [--passcode 提取码]`(打印完整分享链接;加密对象链接自带 `#` 密钥片段);列表 `crab storage shares <id\|文件名>`、撤销 `crab storage unshare <shareId>` |
|
|
46
|
+
| 云盘钥匙管理 | `crab storage key`(查状态)/ `--remember`(解锁一次并记住,之后免口令)/ `--forget`(清除)——见下节 |
|
|
42
47
|
| 删除 | `crab storage rm <id\|文件名>`(进回收站,7 天后自动清理)→ `restore` 恢复 / `purge` 彻底删除 |
|
|
43
48
|
| 看水位 | `crab storage quota`(三来源分项 + 回收站占用 + 对象计数) |
|
|
44
49
|
|
|
45
50
|
`<id|文件名>`:文件名全库精确唯一命中时自动解析为 id(回收站内同样可解析,
|
|
46
51
|
restore/purge 直接用文件名即可);命中多个或查不到时按字面当 id 用——拿不准
|
|
47
|
-
先 `crab storage ls --q <关键字>`
|
|
52
|
+
先 `crab storage ls --q <关键字>` 核对再操作。分类指认同款:id 精确命中优先,
|
|
53
|
+
名称唯一命中其次(`rencat` / `rmcat` / `--category` 都吃 `<id|分类名>`)。
|
|
48
54
|
|
|
49
55
|
## 机器可读输出
|
|
50
56
|
|
|
@@ -53,7 +59,26 @@ restore/purge 直接用文件名即可);命中多个或查不到时按字面
|
|
|
53
59
|
- `storage ls --json` → `{ objects: [...], nextOffset, total }`,条目含
|
|
54
60
|
`id / source / filename / mimeType / sizeBytes / categoryId / tags / isPublic / createdAt`;
|
|
55
61
|
- `storage quota --json` → `{ usedBytes, quotaBytes, bySource: { files, media, mail }, trashBytes, counts }`;
|
|
56
|
-
- `upload` / `categories` / `tags` 的 `--json` 同构(单对象 / 分类数组 /
|
|
62
|
+
- `upload` / `categories` / `tags` / `shares` 的 `--json` 同构(单对象 / 分类数组 /
|
|
63
|
+
标签数组 / 分享数组)。
|
|
64
|
+
|
|
65
|
+
## 云盘口令与本机钥匙
|
|
66
|
+
|
|
67
|
+
files 域是端到端加密:服务器只有口令包裹态的主密钥,**明文密钥只存在于用户
|
|
68
|
+
本机**——CLI 不可能也不允许向服务器索取。涉及加密对象的命令(upload /
|
|
69
|
+
download / share 加密文件)需要云盘口令,来源优先级:
|
|
70
|
+
|
|
71
|
+
1. `--passphrase <口令>`(flag)
|
|
72
|
+
2. 环境变量 `CRAB_PASSPHRASE`(**Agent/脚本推荐**——非交互终端不会弹提问,
|
|
73
|
+
缺失时明确报错)
|
|
74
|
+
3. 交互终端(TTY)隐藏输入提问(人不经管道跑 CLI 时的默认体验)
|
|
75
|
+
|
|
76
|
+
本机钥匙(`crab storage key`):口令解锁成功后可把**主密钥**(非口令)记住到
|
|
77
|
+
本机 `~/.config/crabcloud/drive-key-<账号id>.json`(0600,按账号+环境隔离),
|
|
78
|
+
之后所有命令免口令——等同 Web 端「记住此设备」。`--remember` 主动记住、
|
|
79
|
+
`--forget` 清除、默认查状态;Web 端轮换口令/恢复码后本机钥匙自动验签失效并
|
|
80
|
+
回落口令,无需手工干预。Agent 视角:**用户没让你动钥匙设置时不要主动调
|
|
81
|
+
`storage key`**;用户口令遗忘是恢复码流程(Web 端),CLI 侧无法代找回。
|
|
57
82
|
|
|
58
83
|
## 纪律
|
|
59
84
|
|
|
@@ -61,7 +86,11 @@ restore/purge 直接用文件名即可);命中多个或查不到时按字面
|
|
|
61
86
|
确认后再执行;收回用 `unpublish`(已缓存浏览器至多 5 分钟内失效)。
|
|
62
87
|
- **share 是可控分享**:默认 7 天有效、可选提取码、随时 `unshare` 撤销;加密
|
|
63
88
|
对象的分享链接自带解密钥匙(`#` 后缀)——泄露链接即泄露文件,外发前向
|
|
64
|
-
|
|
89
|
+
用户复述有效期与提取码设置,确认后再执行。收件人在浏览器打开加密分享链接
|
|
90
|
+
会看到内联解密落地页(图片/视频/音频/PDF/文本直接预览,其余类型仅下载)。
|
|
91
|
+
- **提取码错误重试有服务端限流**:同一链接+来源 IP 错误次数超限返回 429
|
|
92
|
+
(退出码 1,消息「尝试过于频繁」)——**不要程序化暴力重试**,如实转述用户
|
|
93
|
+
等待即可;凭链接下载时提取码用 `--passcode` 正确携带。
|
|
65
94
|
- **purge 是彻底删除**,不可恢复;用户没明说「彻底删除」时一律用 `rm`
|
|
66
95
|
(回收站保留 7 天可找回)。
|
|
67
96
|
- 邮件附件与用户上传同账:水位告急时用 `quota` 分项定位来源再建议清理。
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crabcloud/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Crab Cloud CLI (crab)
|
|
5
|
+
"description": "Crab Cloud CLI (crab) — agent-first entry to your personal cloud, with skills bundled for Claude Code / Codex / ZCode.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"crab": "dist/crab.js"
|
|
8
8
|
},
|