@crabcloud/cli 0.1.7 → 0.1.8
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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { hostname, homedir as homedir2 } from "node:os";
|
|
5
5
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6
|
-
import { join as join2 } from "node:path";
|
|
6
|
+
import { basename, join as join2 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// ../../node_modules/.bun/zod@4.6.5/node_modules/zod/v4/core/util.js
|
|
9
9
|
function getEnumValues(entries) {
|
|
@@ -5724,6 +5724,9 @@ function superRefine(fn, params) {
|
|
|
5724
5724
|
var accountDomain = "crabcloud.cc";
|
|
5725
5725
|
var adminDomain = `admin.${accountDomain}`;
|
|
5726
5726
|
var storageDomain = `storage.${accountDomain}`;
|
|
5727
|
+
function storagePublicUrl(objectId) {
|
|
5728
|
+
return `https://${storageDomain}/o/${objectId}`;
|
|
5729
|
+
}
|
|
5727
5730
|
var clientTypes = ["web", "agent", "miniapp", "native"];
|
|
5728
5731
|
var scopes = [
|
|
5729
5732
|
"account.read",
|
|
@@ -5791,7 +5794,7 @@ var registerInput = object({
|
|
|
5791
5794
|
code: string2().trim().regex(/^\d{6}$/),
|
|
5792
5795
|
displayName: string2().trim().min(1).max(80),
|
|
5793
5796
|
password: string2().min(8).max(256),
|
|
5794
|
-
invite: string2().trim().
|
|
5797
|
+
invite: string2().trim().max(64).transform((v) => v.toUpperCase()).optional()
|
|
5795
5798
|
});
|
|
5796
5799
|
var loginInput = object({
|
|
5797
5800
|
username: string2().trim().toLowerCase().min(3).max(254),
|
|
@@ -6209,6 +6212,7 @@ function exitCodeForApiError(body) {
|
|
|
6209
6212
|
case "bad_request":
|
|
6210
6213
|
return EXIT_USAGE;
|
|
6211
6214
|
case "insufficient_credits":
|
|
6215
|
+
case "storage.quotaExceeded":
|
|
6212
6216
|
return EXIT_QUOTA;
|
|
6213
6217
|
default:
|
|
6214
6218
|
return 1;
|
|
@@ -6268,6 +6272,33 @@ var usage = `crab — Crab Cloud CLI(Agent-first 入口)
|
|
|
6268
6272
|
[--in-reply-to <message-id>] 回信线程锚点(原信 RFC Message-ID,crab mail read 可见)
|
|
6269
6273
|
发送邮件:平台内(@crabcloud.cc)免费,外部出站 2 积分/封
|
|
6270
6274
|
mail archive|trash|delete <id> 移夹 / 废纸篓 / 彻底删除(delete 不可恢复)
|
|
6275
|
+
contacts list [query] [--offset N] [--json] 列出联系人(地址前缀/显示名匹配)
|
|
6276
|
+
contacts add <address> [--name 名] [--note 备注] [--json]
|
|
6277
|
+
添加联系人(站内地址自动关联对方账号)
|
|
6278
|
+
contacts rm <id|address> 删除联系人
|
|
6279
|
+
contacts invite [--reset] [--json] 我的邀请码、链接与统计(--reset 重置,旧链接失效)
|
|
6280
|
+
contacts invitees [--offset N] [--json]
|
|
6281
|
+
我邀请的人(直接下级 + 间接计数)
|
|
6282
|
+
storage ls [--q 关键字] [--source files|media|mail] [--category <id|none>]
|
|
6283
|
+
[--type doc|image|video|archive|code] [--tag a,b] [--trash]
|
|
6284
|
+
[--shared] [--recent] [--limit N] [--offset N] [--json]
|
|
6285
|
+
列出云盘对象(跨来源统一视图,能力与 Web 云盘一致)
|
|
6286
|
+
storage upload <file...> [--json] 上传一个或多个文件(直传优先,自动回退中转)
|
|
6287
|
+
storage download <id|文件名> [-o 输出路径]
|
|
6288
|
+
下载对象内容(默认按原文件名存到当前目录)
|
|
6289
|
+
storage rename <id|文件名> <新名称> 重命名
|
|
6290
|
+
storage tag <id|文件名> --add a,b [--remove c,d]
|
|
6291
|
+
增删标签
|
|
6292
|
+
storage category <id|文件名> <categoryId|none>
|
|
6293
|
+
设置分类(分类 id 见 storage categories)
|
|
6294
|
+
storage publish|unpublish <id|文件名>
|
|
6295
|
+
公开 / 收回公开(公开时打印公开直链)
|
|
6296
|
+
storage rm|restore|purge <id|文件名>
|
|
6297
|
+
回收站 / 恢复 / 彻底删除(purge 不可恢复)
|
|
6298
|
+
storage quota [--json] 存储水位(统一容量账:文件/素材/邮件附件一本账)
|
|
6299
|
+
storage categories [--json] 分类列表
|
|
6300
|
+
storage mkcat <名称> 新建分类
|
|
6301
|
+
storage tags [--json] 标签汇总
|
|
6271
6302
|
skills install|list|uninstall [--agent claude,zcode,codex,agents]
|
|
6272
6303
|
skills 管理(claude/zcode/agents 复制目录,codex 写 AGENTS.md 段)
|
|
6273
6304
|
logout 撤销当前 Agent Token 并清除本地凭证
|
|
@@ -6594,6 +6625,582 @@ async function mailMove(action, id) {
|
|
|
6594
6625
|
console.log(`✓ ${action === "archive" ? "已归档" : action === "trash" ? "已移入废纸篓" : "已彻底删除"}(${id})`);
|
|
6595
6626
|
return 0;
|
|
6596
6627
|
}
|
|
6628
|
+
function contactDisplayName(contact) {
|
|
6629
|
+
return contact.displayName ?? contact.resolvedName ?? contact.address;
|
|
6630
|
+
}
|
|
6631
|
+
async function contactsList(query) {
|
|
6632
|
+
const credentials = requireCredentials();
|
|
6633
|
+
if (typeof credentials === "number")
|
|
6634
|
+
return credentials;
|
|
6635
|
+
const params = new URLSearchParams;
|
|
6636
|
+
if (query)
|
|
6637
|
+
params.set("query", query);
|
|
6638
|
+
const offset = readFlag("--offset");
|
|
6639
|
+
if (offset)
|
|
6640
|
+
params.set("offset", offset);
|
|
6641
|
+
params.set("limit", readFlag("--limit") ?? "30");
|
|
6642
|
+
const res = await api2(credentials.apiBase, `/v1/contacts?${params}`, { headers: authHeaders(credentials) });
|
|
6643
|
+
if (res.status !== 200) {
|
|
6644
|
+
console.error(errorMessage(res.body, "获取联系人失败。"));
|
|
6645
|
+
return exitCodeForApiError(res.body);
|
|
6646
|
+
}
|
|
6647
|
+
const page = res.body.data;
|
|
6648
|
+
if (hasFlag("--json")) {
|
|
6649
|
+
console.log(JSON.stringify(page, null, 2));
|
|
6650
|
+
return 0;
|
|
6651
|
+
}
|
|
6652
|
+
if (page.items.length === 0) {
|
|
6653
|
+
console.log("还没有联系人。");
|
|
6654
|
+
return 0;
|
|
6655
|
+
}
|
|
6656
|
+
for (const contact of page.items) {
|
|
6657
|
+
const name = contactDisplayName(contact);
|
|
6658
|
+
const address = name === contact.address ? "" : ` <${contact.address}>`;
|
|
6659
|
+
const linked = contact.linkedUsername ? ` [站内 ${contact.linkedUsername}]` : "";
|
|
6660
|
+
console.log(`${contact.id} ${name}${address}${linked}`);
|
|
6661
|
+
}
|
|
6662
|
+
console.log(`共 ${page.total} 条`);
|
|
6663
|
+
if (page.nextOffset !== null)
|
|
6664
|
+
console.log(`(还有更多:contacts list --offset ${page.nextOffset})`);
|
|
6665
|
+
return 0;
|
|
6666
|
+
}
|
|
6667
|
+
async function contactsAdd(address) {
|
|
6668
|
+
if (!address) {
|
|
6669
|
+
console.error("用法:crab contacts add <address> [--name 显示名] [--note 备注] [--json]");
|
|
6670
|
+
return EXIT_USAGE;
|
|
6671
|
+
}
|
|
6672
|
+
const credentials = requireCredentials();
|
|
6673
|
+
if (typeof credentials === "number")
|
|
6674
|
+
return credentials;
|
|
6675
|
+
const body = { address };
|
|
6676
|
+
const name = readFlag("--name");
|
|
6677
|
+
if (name)
|
|
6678
|
+
body.displayName = name;
|
|
6679
|
+
const note = readFlag("--note");
|
|
6680
|
+
if (note)
|
|
6681
|
+
body.note = note;
|
|
6682
|
+
const res = await api2(credentials.apiBase, "/v1/contacts", {
|
|
6683
|
+
method: "POST",
|
|
6684
|
+
headers: authHeaders(credentials),
|
|
6685
|
+
body: JSON.stringify(body)
|
|
6686
|
+
});
|
|
6687
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
6688
|
+
console.error(errorMessage(res.body, "添加失败。"));
|
|
6689
|
+
return exitCodeForApiError(res.body);
|
|
6690
|
+
}
|
|
6691
|
+
const contact = res.body.data;
|
|
6692
|
+
if (hasFlag("--json")) {
|
|
6693
|
+
console.log(JSON.stringify(contact, null, 2));
|
|
6694
|
+
return 0;
|
|
6695
|
+
}
|
|
6696
|
+
console.log(`✓ 已添加:${contactDisplayName(contact)} <${contact.address}>(id ${contact.id})`);
|
|
6697
|
+
return 0;
|
|
6698
|
+
}
|
|
6699
|
+
async function contactsRm(idOrAddress) {
|
|
6700
|
+
if (!idOrAddress) {
|
|
6701
|
+
console.error("用法:crab contacts rm <id|address>");
|
|
6702
|
+
return EXIT_USAGE;
|
|
6703
|
+
}
|
|
6704
|
+
const credentials = requireCredentials();
|
|
6705
|
+
if (typeof credentials === "number")
|
|
6706
|
+
return credentials;
|
|
6707
|
+
let id = idOrAddress;
|
|
6708
|
+
if (idOrAddress.includes("@")) {
|
|
6709
|
+
const res = await api2(credentials.apiBase, `/v1/contacts?query=${encodeURIComponent(idOrAddress)}&limit=50`, {
|
|
6710
|
+
headers: authHeaders(credentials)
|
|
6711
|
+
});
|
|
6712
|
+
if (res.status !== 200) {
|
|
6713
|
+
console.error(errorMessage(res.body, "查找联系人失败。"));
|
|
6714
|
+
return exitCodeForApiError(res.body);
|
|
6715
|
+
}
|
|
6716
|
+
const page = res.body.data;
|
|
6717
|
+
const hit = page.items.find((item) => item.address === idOrAddress.toLowerCase());
|
|
6718
|
+
if (!hit) {
|
|
6719
|
+
console.error("联系人不存在或已删除。");
|
|
6720
|
+
return EXIT_NOT_FOUND;
|
|
6721
|
+
}
|
|
6722
|
+
id = hit.id;
|
|
6723
|
+
}
|
|
6724
|
+
const res = await api2(credentials.apiBase, `/v1/contacts/${encodeURIComponent(id)}`, {
|
|
6725
|
+
method: "DELETE",
|
|
6726
|
+
headers: authHeaders(credentials)
|
|
6727
|
+
});
|
|
6728
|
+
if (res.status !== 200) {
|
|
6729
|
+
console.error(errorMessage(res.body, "删除失败。"));
|
|
6730
|
+
return exitCodeForApiError(res.body);
|
|
6731
|
+
}
|
|
6732
|
+
console.log(`✓ 已删除(${id})`);
|
|
6733
|
+
return 0;
|
|
6734
|
+
}
|
|
6735
|
+
async function contactsInvite(reset) {
|
|
6736
|
+
const credentials = requireCredentials();
|
|
6737
|
+
if (typeof credentials === "number")
|
|
6738
|
+
return credentials;
|
|
6739
|
+
const res = await api2(credentials.apiBase, "/v1/contacts/invite/reset", {
|
|
6740
|
+
method: reset ? "POST" : "GET",
|
|
6741
|
+
headers: authHeaders(credentials)
|
|
6742
|
+
});
|
|
6743
|
+
if (res.status !== 200) {
|
|
6744
|
+
console.error(errorMessage(res.body, "获取邀请链接失败。"));
|
|
6745
|
+
return exitCodeForApiError(res.body);
|
|
6746
|
+
}
|
|
6747
|
+
const overview = res.body.data;
|
|
6748
|
+
if (hasFlag("--json")) {
|
|
6749
|
+
console.log(JSON.stringify(overview, null, 2));
|
|
6750
|
+
return 0;
|
|
6751
|
+
}
|
|
6752
|
+
console.log(`邀请码:${overview.token}`);
|
|
6753
|
+
console.log(`邀请链接:https://crabcloud.cc/register?invite=${overview.token}`);
|
|
6754
|
+
console.log(`已邀请 ${overview.directCount} 人 · 含间接 ${overview.indirectCount} 人`);
|
|
6755
|
+
if (!reset)
|
|
6756
|
+
console.log("(重置链接:crab contacts invite --reset)");
|
|
6757
|
+
return 0;
|
|
6758
|
+
}
|
|
6759
|
+
async function contactsInvitees() {
|
|
6760
|
+
const credentials = requireCredentials();
|
|
6761
|
+
if (typeof credentials === "number")
|
|
6762
|
+
return credentials;
|
|
6763
|
+
const offset = readFlag("--offset");
|
|
6764
|
+
const res = await api2(credentials.apiBase, `/v1/contacts/invitees?offset=${offset ?? "0"}&limit=30`, {
|
|
6765
|
+
headers: authHeaders(credentials)
|
|
6766
|
+
});
|
|
6767
|
+
if (res.status !== 200) {
|
|
6768
|
+
console.error(errorMessage(res.body, "获取邀请列表失败。"));
|
|
6769
|
+
return exitCodeForApiError(res.body);
|
|
6770
|
+
}
|
|
6771
|
+
const page = res.body.data;
|
|
6772
|
+
if (hasFlag("--json")) {
|
|
6773
|
+
console.log(JSON.stringify(page, null, 2));
|
|
6774
|
+
return 0;
|
|
6775
|
+
}
|
|
6776
|
+
if (page.items.length === 0) {
|
|
6777
|
+
console.log("还没有人通过你的链接加入。");
|
|
6778
|
+
return 0;
|
|
6779
|
+
}
|
|
6780
|
+
for (const invitee of page.items) {
|
|
6781
|
+
console.log(`${invitee.username} ${invitee.displayName} ${new Date(invitee.joinedAt).toLocaleDateString("zh-CN")}`);
|
|
6782
|
+
}
|
|
6783
|
+
console.log(`直接 ${page.directTotal} · 间接 ${page.indirectTotal}`);
|
|
6784
|
+
if (page.nextOffset !== null)
|
|
6785
|
+
console.log(`(还有更多:contacts invitees --offset ${page.nextOffset})`);
|
|
6786
|
+
return 0;
|
|
6787
|
+
}
|
|
6788
|
+
var STORAGE_SOURCES = ["files", "media", "mail"];
|
|
6789
|
+
var STORAGE_TYPES = ["doc", "image", "video", "archive", "code"];
|
|
6790
|
+
var MIME_BY_EXT = {
|
|
6791
|
+
png: "image/png",
|
|
6792
|
+
jpg: "image/jpeg",
|
|
6793
|
+
jpeg: "image/jpeg",
|
|
6794
|
+
gif: "image/gif",
|
|
6795
|
+
webp: "image/webp",
|
|
6796
|
+
svg: "image/svg+xml",
|
|
6797
|
+
pdf: "application/pdf",
|
|
6798
|
+
txt: "text/plain",
|
|
6799
|
+
md: "text/markdown",
|
|
6800
|
+
csv: "text/csv",
|
|
6801
|
+
json: "application/json",
|
|
6802
|
+
xml: "application/xml",
|
|
6803
|
+
yaml: "application/x-yaml",
|
|
6804
|
+
yml: "application/x-yaml",
|
|
6805
|
+
js: "text/javascript",
|
|
6806
|
+
zip: "application/zip",
|
|
6807
|
+
gz: "application/gzip",
|
|
6808
|
+
mp3: "audio/mpeg",
|
|
6809
|
+
mp4: "video/mp4"
|
|
6810
|
+
};
|
|
6811
|
+
function mimeFor(filename) {
|
|
6812
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
6813
|
+
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
6814
|
+
}
|
|
6815
|
+
function formatBytesCli(n) {
|
|
6816
|
+
if (n >= 1073741824)
|
|
6817
|
+
return `${(n / 1073741824).toFixed(1)} GB`;
|
|
6818
|
+
if (n >= 1048576)
|
|
6819
|
+
return `${(n / 1048576).toFixed(1)} MB`;
|
|
6820
|
+
if (n >= 1024)
|
|
6821
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
6822
|
+
return `${n} B`;
|
|
6823
|
+
}
|
|
6824
|
+
function printStorageRow(o) {
|
|
6825
|
+
const flags = [
|
|
6826
|
+
o.source !== "files" ? o.source === "media" ? "素材" : "邮件附件" : null,
|
|
6827
|
+
o.isPublic ? "公开" : null
|
|
6828
|
+
].filter(Boolean);
|
|
6829
|
+
console.log(`${o.id} ${o.filename} ${formatBytesCli(o.sizeBytes)} ${formatTime(o.createdAt)}${flags.length > 0 ? ` [${flags.join(" · ")}]` : ""}`);
|
|
6830
|
+
}
|
|
6831
|
+
async function resolveStorageId(credentials, token) {
|
|
6832
|
+
for (const trash of [false, true]) {
|
|
6833
|
+
const params = new URLSearchParams({ limit: "100", q: token });
|
|
6834
|
+
if (trash)
|
|
6835
|
+
params.set("trash", "1");
|
|
6836
|
+
const res = await api2(credentials.apiBase, `/v1/storage?${params}`, {
|
|
6837
|
+
headers: authHeaders(credentials)
|
|
6838
|
+
});
|
|
6839
|
+
if (res.status !== 200) {
|
|
6840
|
+
console.error(errorMessage(res.body, "查找对象失败。"));
|
|
6841
|
+
return exitCodeForApiError(res.body);
|
|
6842
|
+
}
|
|
6843
|
+
const exact = res.body.data.objects.filter((o) => o.filename === token);
|
|
6844
|
+
if (exact.length > 1) {
|
|
6845
|
+
console.error(`文件名「${token}」命中多个对象,请用 id 指定(crab storage ls 查询)。`);
|
|
6846
|
+
return EXIT_USAGE;
|
|
6847
|
+
}
|
|
6848
|
+
if (exact.length === 1)
|
|
6849
|
+
return exact[0].id;
|
|
6850
|
+
}
|
|
6851
|
+
return token;
|
|
6852
|
+
}
|
|
6853
|
+
async function storageList() {
|
|
6854
|
+
const credentials = requireCredentials();
|
|
6855
|
+
if (typeof credentials === "number")
|
|
6856
|
+
return credentials;
|
|
6857
|
+
const params = new URLSearchParams({ limit: readFlag("--limit") ?? "30" });
|
|
6858
|
+
const source = readFlag("--source");
|
|
6859
|
+
if (source) {
|
|
6860
|
+
if (!STORAGE_SOURCES.includes(source)) {
|
|
6861
|
+
console.error(`未知来源:${source}
|
|
6862
|
+
可用:${STORAGE_SOURCES.join(", ")}`);
|
|
6863
|
+
return EXIT_USAGE;
|
|
6864
|
+
}
|
|
6865
|
+
params.set("source", source);
|
|
6866
|
+
}
|
|
6867
|
+
const type = readFlag("--type");
|
|
6868
|
+
if (type) {
|
|
6869
|
+
if (!STORAGE_TYPES.includes(type)) {
|
|
6870
|
+
console.error(`未知类型:${type}
|
|
6871
|
+
可用:${STORAGE_TYPES.join(", ")}`);
|
|
6872
|
+
return EXIT_USAGE;
|
|
6873
|
+
}
|
|
6874
|
+
params.set("type", type);
|
|
6875
|
+
}
|
|
6876
|
+
const category = readFlag("--category");
|
|
6877
|
+
if (category)
|
|
6878
|
+
params.set("category", category);
|
|
6879
|
+
const tags = readFlag("--tag");
|
|
6880
|
+
if (tags)
|
|
6881
|
+
params.set("tags", tags);
|
|
6882
|
+
if (hasFlag("--trash"))
|
|
6883
|
+
params.set("trash", "1");
|
|
6884
|
+
if (hasFlag("--shared"))
|
|
6885
|
+
params.set("public", "1");
|
|
6886
|
+
if (hasFlag("--recent"))
|
|
6887
|
+
params.set("since", String(Date.now() - 604800000));
|
|
6888
|
+
const offset = readFlag("--offset");
|
|
6889
|
+
if (offset)
|
|
6890
|
+
params.set("offset", offset);
|
|
6891
|
+
const q = readFlag("--q");
|
|
6892
|
+
if (q)
|
|
6893
|
+
params.set("q", q);
|
|
6894
|
+
const res = await api2(credentials.apiBase, `/v1/storage?${params}`, { headers: authHeaders(credentials) });
|
|
6895
|
+
if (res.status !== 200) {
|
|
6896
|
+
console.error(errorMessage(res.body, "获取云盘列表失败。"));
|
|
6897
|
+
return exitCodeForApiError(res.body);
|
|
6898
|
+
}
|
|
6899
|
+
const page = res.body.data;
|
|
6900
|
+
if (hasFlag("--json")) {
|
|
6901
|
+
console.log(JSON.stringify(page, null, 2));
|
|
6902
|
+
return 0;
|
|
6903
|
+
}
|
|
6904
|
+
if (page.objects.length === 0) {
|
|
6905
|
+
console.log("云盘里没有匹配的对象。");
|
|
6906
|
+
return 0;
|
|
6907
|
+
}
|
|
6908
|
+
for (const object of page.objects)
|
|
6909
|
+
printStorageRow(object);
|
|
6910
|
+
if (page.nextOffset !== null)
|
|
6911
|
+
console.log(`(还有更多:storage ls --offset ${page.nextOffset})`);
|
|
6912
|
+
return 0;
|
|
6913
|
+
}
|
|
6914
|
+
async function storageUploadOne(credentials, filePath) {
|
|
6915
|
+
let buf;
|
|
6916
|
+
try {
|
|
6917
|
+
buf = readFileSync2(filePath);
|
|
6918
|
+
} catch {
|
|
6919
|
+
console.error(`读取文件失败:${filePath}`);
|
|
6920
|
+
return EXIT_USAGE;
|
|
6921
|
+
}
|
|
6922
|
+
const filename = basename(filePath);
|
|
6923
|
+
const mimeType = mimeFor(filename);
|
|
6924
|
+
const presign = await api2(credentials.apiBase, "/v1/storage/presign", {
|
|
6925
|
+
method: "POST",
|
|
6926
|
+
headers: authHeaders(credentials),
|
|
6927
|
+
body: JSON.stringify({ filename, mimeType, sizeBytes: buf.byteLength })
|
|
6928
|
+
});
|
|
6929
|
+
if (presign.status === 200) {
|
|
6930
|
+
const { fileId, uploadUrl } = presign.body.data;
|
|
6931
|
+
const put = await fetch(uploadUrl, { method: "PUT", body: new Uint8Array(buf), headers: { "content-type": mimeType } });
|
|
6932
|
+
if (!put.ok) {
|
|
6933
|
+
console.error(`直传失败(${put.status}):${filename}`);
|
|
6934
|
+
return 1;
|
|
6935
|
+
}
|
|
6936
|
+
const complete = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(fileId)}/complete`, {
|
|
6937
|
+
method: "POST",
|
|
6938
|
+
headers: authHeaders(credentials),
|
|
6939
|
+
body: "{}"
|
|
6940
|
+
});
|
|
6941
|
+
if (complete.status !== 200 && complete.status !== 201) {
|
|
6942
|
+
console.error(errorMessage(complete.body, `记账失败:${filename}`));
|
|
6943
|
+
return exitCodeForApiError(complete.body);
|
|
6944
|
+
}
|
|
6945
|
+
const object = complete.body.data;
|
|
6946
|
+
if (hasFlag("--json"))
|
|
6947
|
+
console.log(JSON.stringify(object, null, 2));
|
|
6948
|
+
else
|
|
6949
|
+
console.log(`✓ 已上传:${object.filename}(id ${object.id})`);
|
|
6950
|
+
return 0;
|
|
6951
|
+
}
|
|
6952
|
+
if (presign.body?.error?.code === "presign_unavailable") {
|
|
6953
|
+
const form = new FormData;
|
|
6954
|
+
form.append("file", new Blob([new Uint8Array(buf)], { type: mimeType }), filename);
|
|
6955
|
+
const res = await fetch(`${credentials.apiBase}/v1/storage`, {
|
|
6956
|
+
method: "POST",
|
|
6957
|
+
headers: authHeaders(credentials),
|
|
6958
|
+
body: form
|
|
6959
|
+
});
|
|
6960
|
+
const body = await res.json().catch(() => ({}));
|
|
6961
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
6962
|
+
console.error(body.error?.message ?? `上传失败:${filename}`);
|
|
6963
|
+
return exitCodeForApiError(body);
|
|
6964
|
+
}
|
|
6965
|
+
if (hasFlag("--json"))
|
|
6966
|
+
console.log(JSON.stringify(body.data, null, 2));
|
|
6967
|
+
else
|
|
6968
|
+
console.log(`✓ 已上传:${body.data?.filename}(id ${body.data?.id})`);
|
|
6969
|
+
return 0;
|
|
6970
|
+
}
|
|
6971
|
+
console.error(errorMessage(presign.body, `上传失败:${filename}`));
|
|
6972
|
+
return exitCodeForApiError(presign.body);
|
|
6973
|
+
}
|
|
6974
|
+
async function storageUpload(paths) {
|
|
6975
|
+
const files = paths.filter((p) => !p.startsWith("--"));
|
|
6976
|
+
if (files.length === 0) {
|
|
6977
|
+
console.error("用法:crab storage upload <file...> [--json]");
|
|
6978
|
+
return EXIT_USAGE;
|
|
6979
|
+
}
|
|
6980
|
+
const credentials = requireCredentials();
|
|
6981
|
+
if (typeof credentials === "number")
|
|
6982
|
+
return credentials;
|
|
6983
|
+
let code = 0;
|
|
6984
|
+
for (const file of files) {
|
|
6985
|
+
const result = await storageUploadOne(credentials, file);
|
|
6986
|
+
if (result !== 0 && code === 0)
|
|
6987
|
+
code = result;
|
|
6988
|
+
if (result === EXIT_AUTH || result === EXIT_SCOPE)
|
|
6989
|
+
return result;
|
|
6990
|
+
}
|
|
6991
|
+
return code;
|
|
6992
|
+
}
|
|
6993
|
+
async function storageDownload(token) {
|
|
6994
|
+
if (!token) {
|
|
6995
|
+
console.error("用法:crab storage download <id|文件名> [-o 输出路径]");
|
|
6996
|
+
return EXIT_USAGE;
|
|
6997
|
+
}
|
|
6998
|
+
const credentials = requireCredentials();
|
|
6999
|
+
if (typeof credentials === "number")
|
|
7000
|
+
return credentials;
|
|
7001
|
+
const id = await resolveStorageId(credentials, token);
|
|
7002
|
+
if (typeof id === "number")
|
|
7003
|
+
return id;
|
|
7004
|
+
const res = await fetch(`${credentials.apiBase}/v1/storage/${encodeURIComponent(id)}/content`, {
|
|
7005
|
+
headers: authHeaders(credentials)
|
|
7006
|
+
});
|
|
7007
|
+
if (!res.ok) {
|
|
7008
|
+
const body = await res.json().catch(() => ({}));
|
|
7009
|
+
console.error(errorMessage(body, "下载失败。"));
|
|
7010
|
+
return exitCodeForApiError(body);
|
|
7011
|
+
}
|
|
7012
|
+
const disposition = res.headers.get("content-disposition") ?? "";
|
|
7013
|
+
const match = disposition.match(/filename\*=UTF-8''([^;]+)/);
|
|
7014
|
+
const fallbackName = match ? decodeURIComponent(match[1]) : `${id}.bin`;
|
|
7015
|
+
const out = readFlag("-o") ?? readFlag("--out") ?? fallbackName;
|
|
7016
|
+
writeFileSync2(out, Buffer.from(await res.arrayBuffer()));
|
|
7017
|
+
console.log(`✓ 已下载:${out}(${formatBytesCli(Number(res.headers.get("content-length") ?? 0))})`);
|
|
7018
|
+
return 0;
|
|
7019
|
+
}
|
|
7020
|
+
async function storagePatch(credentials, token, patch) {
|
|
7021
|
+
const id = await resolveStorageId(credentials, token);
|
|
7022
|
+
if (typeof id === "number")
|
|
7023
|
+
return id;
|
|
7024
|
+
const res = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(id)}`, {
|
|
7025
|
+
method: "PATCH",
|
|
7026
|
+
headers: authHeaders(credentials),
|
|
7027
|
+
body: JSON.stringify(patch)
|
|
7028
|
+
});
|
|
7029
|
+
if (res.status !== 200) {
|
|
7030
|
+
console.error(errorMessage(res.body, "更新失败。"));
|
|
7031
|
+
return exitCodeForApiError(res.body);
|
|
7032
|
+
}
|
|
7033
|
+
return res.body.data;
|
|
7034
|
+
}
|
|
7035
|
+
async function storageRename(token, name) {
|
|
7036
|
+
if (!token || !name) {
|
|
7037
|
+
console.error("用法:crab storage rename <id|文件名> <新名称>");
|
|
7038
|
+
return EXIT_USAGE;
|
|
7039
|
+
}
|
|
7040
|
+
const credentials = requireCredentials();
|
|
7041
|
+
if (typeof credentials === "number")
|
|
7042
|
+
return credentials;
|
|
7043
|
+
const result = await storagePatch(credentials, token, { filename: name });
|
|
7044
|
+
if (typeof result === "number")
|
|
7045
|
+
return result;
|
|
7046
|
+
console.log(`✓ 已重命名:${result.filename}(id ${result.id})`);
|
|
7047
|
+
return 0;
|
|
7048
|
+
}
|
|
7049
|
+
async function storageTag(token) {
|
|
7050
|
+
if (!token || !hasFlag("--add") && !hasFlag("--remove")) {
|
|
7051
|
+
console.error("用法:crab storage tag <id|文件名> --add a,b [--remove c,d]");
|
|
7052
|
+
return EXIT_USAGE;
|
|
7053
|
+
}
|
|
7054
|
+
const credentials = requireCredentials();
|
|
7055
|
+
if (typeof credentials === "number")
|
|
7056
|
+
return credentials;
|
|
7057
|
+
const add = (readFlag("--add") ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
7058
|
+
const remove = (readFlag("--remove") ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
7059
|
+
const patch = {};
|
|
7060
|
+
if (add.length > 0)
|
|
7061
|
+
patch.addTags = add;
|
|
7062
|
+
if (remove.length > 0)
|
|
7063
|
+
patch.removeTags = remove;
|
|
7064
|
+
const result = await storagePatch(credentials, token, patch);
|
|
7065
|
+
if (typeof result === "number")
|
|
7066
|
+
return result;
|
|
7067
|
+
console.log(`✓ 标签已更新:${result.filename} [${result.tags.join(", ") || "无"}]`);
|
|
7068
|
+
return 0;
|
|
7069
|
+
}
|
|
7070
|
+
async function storageCategory(token, value) {
|
|
7071
|
+
if (!token || !value) {
|
|
7072
|
+
console.error("用法:crab storage category <id|文件名> <categoryId|none>(分类 id 见 storage categories)");
|
|
7073
|
+
return EXIT_USAGE;
|
|
7074
|
+
}
|
|
7075
|
+
const credentials = requireCredentials();
|
|
7076
|
+
if (typeof credentials === "number")
|
|
7077
|
+
return credentials;
|
|
7078
|
+
const result = await storagePatch(credentials, token, { categoryId: value === "none" ? null : value });
|
|
7079
|
+
if (typeof result === "number")
|
|
7080
|
+
return result;
|
|
7081
|
+
console.log(`✓ 分类已更新:${result.filename}`);
|
|
7082
|
+
return 0;
|
|
7083
|
+
}
|
|
7084
|
+
async function storagePublish(token, publish) {
|
|
7085
|
+
if (!token) {
|
|
7086
|
+
console.error(`用法:crab storage ${publish ? "publish" : "unpublish"} <id|文件名>`);
|
|
7087
|
+
return EXIT_USAGE;
|
|
7088
|
+
}
|
|
7089
|
+
const credentials = requireCredentials();
|
|
7090
|
+
if (typeof credentials === "number")
|
|
7091
|
+
return credentials;
|
|
7092
|
+
const result = await storagePatch(credentials, token, { isPublic: publish });
|
|
7093
|
+
if (typeof result === "number")
|
|
7094
|
+
return result;
|
|
7095
|
+
console.log(`✓ ${publish ? "已公开" : "已收回公开"}:${result.filename}`);
|
|
7096
|
+
if (publish)
|
|
7097
|
+
console.log(` 公开链接:${storagePublicUrl(result.id)}`);
|
|
7098
|
+
return 0;
|
|
7099
|
+
}
|
|
7100
|
+
async function storageMove(action, token) {
|
|
7101
|
+
if (!token) {
|
|
7102
|
+
console.error(`用法:crab storage ${action} <id|文件名>`);
|
|
7103
|
+
return EXIT_USAGE;
|
|
7104
|
+
}
|
|
7105
|
+
const credentials = requireCredentials();
|
|
7106
|
+
if (typeof credentials === "number")
|
|
7107
|
+
return credentials;
|
|
7108
|
+
const id = await resolveStorageId(credentials, token);
|
|
7109
|
+
if (typeof id === "number")
|
|
7110
|
+
return id;
|
|
7111
|
+
const res = await api2(credentials.apiBase, `/v1/storage/${encodeURIComponent(id)}${action === "restore" ? "/restore" : ""}${action === "purge" ? "/purge" : ""}`, { method: action === "restore" ? "POST" : "DELETE", headers: authHeaders(credentials) });
|
|
7112
|
+
if (res.status !== 200) {
|
|
7113
|
+
console.error(errorMessage(res.body, "操作失败。"));
|
|
7114
|
+
return exitCodeForApiError(res.body);
|
|
7115
|
+
}
|
|
7116
|
+
console.log(`✓ ${action === "rm" ? "已移入回收站(7 天后自动清理)" : action === "restore" ? "已恢复" : "已彻底删除"}(${id})`);
|
|
7117
|
+
return 0;
|
|
7118
|
+
}
|
|
7119
|
+
async function storageQuota() {
|
|
7120
|
+
const credentials = requireCredentials();
|
|
7121
|
+
if (typeof credentials === "number")
|
|
7122
|
+
return credentials;
|
|
7123
|
+
const res = await api2(credentials.apiBase, "/v1/storage/quota", { headers: authHeaders(credentials) });
|
|
7124
|
+
if (res.status !== 200) {
|
|
7125
|
+
console.error(errorMessage(res.body, "获取水位失败。"));
|
|
7126
|
+
return exitCodeForApiError(res.body);
|
|
7127
|
+
}
|
|
7128
|
+
const quota = res.body.data;
|
|
7129
|
+
if (hasFlag("--json")) {
|
|
7130
|
+
console.log(JSON.stringify(quota, null, 2));
|
|
7131
|
+
return 0;
|
|
7132
|
+
}
|
|
7133
|
+
const pct = quota.quotaBytes > 0 ? (quota.usedBytes / quota.quotaBytes * 100).toFixed(1) : "0";
|
|
7134
|
+
console.log(`已用 ${formatBytesCli(quota.usedBytes)} / ${formatBytesCli(quota.quotaBytes)}(${pct}%)`);
|
|
7135
|
+
console.log(` 文件 ${formatBytesCli(quota.bySource.files)} · 素材 ${formatBytesCli(quota.bySource.media)} · 邮件附件 ${formatBytesCli(quota.bySource.mail)} · 回收站 ${formatBytesCli(quota.trashBytes)}`);
|
|
7136
|
+
console.log(` 对象数:全部 ${quota.counts.all ?? 0} · 素材 ${quota.counts.media ?? 0} · 邮件附件 ${quota.counts.mail ?? 0} · 共享 ${quota.counts.shared ?? 0} · 回收站 ${quota.counts.trash ?? 0}`);
|
|
7137
|
+
return 0;
|
|
7138
|
+
}
|
|
7139
|
+
async function storageCategories() {
|
|
7140
|
+
const credentials = requireCredentials();
|
|
7141
|
+
if (typeof credentials === "number")
|
|
7142
|
+
return credentials;
|
|
7143
|
+
const res = await api2(credentials.apiBase, "/v1/storage/categories", { headers: authHeaders(credentials) });
|
|
7144
|
+
if (res.status !== 200) {
|
|
7145
|
+
console.error(errorMessage(res.body, "获取分类失败。"));
|
|
7146
|
+
return exitCodeForApiError(res.body);
|
|
7147
|
+
}
|
|
7148
|
+
const rows = res.body.data.categories;
|
|
7149
|
+
if (hasFlag("--json")) {
|
|
7150
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7151
|
+
return 0;
|
|
7152
|
+
}
|
|
7153
|
+
if (rows.length === 0) {
|
|
7154
|
+
console.log("还没有分类(crab storage mkcat <名称> 创建)。");
|
|
7155
|
+
return 0;
|
|
7156
|
+
}
|
|
7157
|
+
for (const row of rows)
|
|
7158
|
+
console.log(`${row.id} ${row.name} (${row.fileCount})`);
|
|
7159
|
+
return 0;
|
|
7160
|
+
}
|
|
7161
|
+
async function storageMkCat(name) {
|
|
7162
|
+
if (!name) {
|
|
7163
|
+
console.error("用法:crab storage mkcat <名称>");
|
|
7164
|
+
return EXIT_USAGE;
|
|
7165
|
+
}
|
|
7166
|
+
const credentials = requireCredentials();
|
|
7167
|
+
if (typeof credentials === "number")
|
|
7168
|
+
return credentials;
|
|
7169
|
+
const res = await api2(credentials.apiBase, "/v1/storage/categories", {
|
|
7170
|
+
method: "POST",
|
|
7171
|
+
headers: authHeaders(credentials),
|
|
7172
|
+
body: JSON.stringify({ name })
|
|
7173
|
+
});
|
|
7174
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
7175
|
+
console.error(errorMessage(res.body, "创建分类失败。"));
|
|
7176
|
+
return exitCodeForApiError(res.body);
|
|
7177
|
+
}
|
|
7178
|
+
const row = res.body.data;
|
|
7179
|
+
console.log(`✓ 已创建分类:${row.name}(id ${row.id})`);
|
|
7180
|
+
return 0;
|
|
7181
|
+
}
|
|
7182
|
+
async function storageTags() {
|
|
7183
|
+
const credentials = requireCredentials();
|
|
7184
|
+
if (typeof credentials === "number")
|
|
7185
|
+
return credentials;
|
|
7186
|
+
const res = await api2(credentials.apiBase, "/v1/storage/tags", { headers: authHeaders(credentials) });
|
|
7187
|
+
if (res.status !== 200) {
|
|
7188
|
+
console.error(errorMessage(res.body, "获取标签失败。"));
|
|
7189
|
+
return exitCodeForApiError(res.body);
|
|
7190
|
+
}
|
|
7191
|
+
const rows = res.body.data;
|
|
7192
|
+
if (hasFlag("--json")) {
|
|
7193
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
7194
|
+
return 0;
|
|
7195
|
+
}
|
|
7196
|
+
if (rows.tags.length === 0) {
|
|
7197
|
+
console.log("还没有标签(crab storage tag <id> --add a,b 添加)。");
|
|
7198
|
+
return 0;
|
|
7199
|
+
}
|
|
7200
|
+
for (const row of rows.tags)
|
|
7201
|
+
console.log(`${row.tag} (${row.count})`);
|
|
7202
|
+
return 0;
|
|
7203
|
+
}
|
|
6597
7204
|
async function init() {
|
|
6598
7205
|
if (loadCredentials()) {
|
|
6599
7206
|
console.log("已登录(crab whoami 查看身份)。");
|
|
@@ -6666,6 +7273,58 @@ switch (command) {
|
|
|
6666
7273
|
process.exit(await mailMove(sub, args[2]));
|
|
6667
7274
|
console.error(sub ? `未知子命令:mail ${sub}
|
|
6668
7275
|
` : `缺少子命令:mail list | read | search | send | archive | trash | delete
|
|
7276
|
+
`);
|
|
7277
|
+
console.log(usage);
|
|
7278
|
+
process.exit(EXIT_USAGE);
|
|
7279
|
+
}
|
|
7280
|
+
case "contacts": {
|
|
7281
|
+
const sub = args[1];
|
|
7282
|
+
if (sub === "list")
|
|
7283
|
+
process.exit(await contactsList(args.slice(2).join(" ") || undefined));
|
|
7284
|
+
if (sub === "add")
|
|
7285
|
+
process.exit(await contactsAdd(args[2]));
|
|
7286
|
+
if (sub === "rm")
|
|
7287
|
+
process.exit(await contactsRm(args[2]));
|
|
7288
|
+
if (sub === "invite")
|
|
7289
|
+
process.exit(await contactsInvite(hasFlag("--reset")));
|
|
7290
|
+
if (sub === "invitees")
|
|
7291
|
+
process.exit(await contactsInvitees());
|
|
7292
|
+
console.error(sub ? `未知子命令:contacts ${sub}
|
|
7293
|
+
` : `缺少子命令:contacts list | add | rm | invite | invitees
|
|
7294
|
+
`);
|
|
7295
|
+
console.log(usage);
|
|
7296
|
+
process.exit(EXIT_USAGE);
|
|
7297
|
+
}
|
|
7298
|
+
case "storage": {
|
|
7299
|
+
const sub = args[1];
|
|
7300
|
+
if (sub === "ls")
|
|
7301
|
+
process.exit(await storageList());
|
|
7302
|
+
if (sub === "upload")
|
|
7303
|
+
process.exit(await storageUpload(args.slice(2)));
|
|
7304
|
+
if (sub === "download")
|
|
7305
|
+
process.exit(await storageDownload(args[2]));
|
|
7306
|
+
if (sub === "rename")
|
|
7307
|
+
process.exit(await storageRename(args[2], args[3]));
|
|
7308
|
+
if (sub === "tag")
|
|
7309
|
+
process.exit(await storageTag(args[2]));
|
|
7310
|
+
if (sub === "category")
|
|
7311
|
+
process.exit(await storageCategory(args[2], args[3]));
|
|
7312
|
+
if (sub === "publish")
|
|
7313
|
+
process.exit(await storagePublish(args[2], true));
|
|
7314
|
+
if (sub === "unpublish")
|
|
7315
|
+
process.exit(await storagePublish(args[2], false));
|
|
7316
|
+
if (sub === "rm" || sub === "restore" || sub === "purge")
|
|
7317
|
+
process.exit(await storageMove(sub, args[2]));
|
|
7318
|
+
if (sub === "quota")
|
|
7319
|
+
process.exit(await storageQuota());
|
|
7320
|
+
if (sub === "categories")
|
|
7321
|
+
process.exit(await storageCategories());
|
|
7322
|
+
if (sub === "mkcat")
|
|
7323
|
+
process.exit(await storageMkCat(args.slice(2).join(" ") || undefined));
|
|
7324
|
+
if (sub === "tags")
|
|
7325
|
+
process.exit(await storageTags());
|
|
7326
|
+
console.error(sub ? `未知子命令:storage ${sub}
|
|
7327
|
+
` : `缺少子命令:storage ls | upload | download | rename | tag | category | publish | unpublish | rm | restore | purge | quota | categories | mkcat | tags
|
|
6669
7328
|
`);
|
|
6670
7329
|
console.log(usage);
|
|
6671
7330
|
process.exit(EXIT_USAGE);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: crabcloud
|
|
3
|
-
description: Manage the user's Crab Cloud personal cloud account and agent tokens through the `crab` CLI — check identity/subscription/credits, list and revoke authorized agent tokens, and re-authorize with specific scopes or scope groups. Use this skill whenever the user asks about their Crab Cloud account ("我的账号信息", "who am I on crabcloud"), wants to see or clean up authorized agents/tokens ("看看我授权了哪些 agent", "revoke that old token", "撤销那个旧令牌"), or asks how to connect their coding agent to Crab Cloud — even if they never say "crab" or "Crab Cloud" explicitly.
|
|
3
|
+
description: Manage the user's Crab Cloud personal cloud account and agent tokens through the `crab` CLI — check identity/subscription/credits, list and revoke authorized agent tokens, and re-authorize with specific scopes or scope groups. Use this skill whenever the user asks about their Crab Cloud account ("我的账号信息", "who am I on crabcloud"), wants to see or clean up authorized agents/tokens ("看看我授权了哪些 agent", "revoke that old token", "撤销那个旧令牌"), manages their contacts ("我的联系人", "把 zhangsan 加到联系人", "我的邀请码"), or asks how to connect their coding agent to Crab Cloud — even if they never say "crab" or "Crab Cloud" explicitly. Contacts are managed with `crab contacts …` in this skill; email lives in the crabcloud-mail skill (crab mail …) and the Drive in crabcloud-storage (crab storage …); collab ships in a later phase.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Crab Cloud(平台 / 账号域)
|
|
@@ -8,9 +8,10 @@ description: Manage the user's Crab Cloud personal cloud account and agent token
|
|
|
8
8
|
Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,邮箱 / 云盘 / 协作 /
|
|
9
9
|
联系人是独立应用。`crab` CLI 是 Agent 的稳定接口:凭证在用户本机
|
|
10
10
|
(`~/.config/crabcloud/credentials.json`,0600),服务端只存令牌哈希、全程审计。
|
|
11
|
-
本 skill
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
本 skill 覆盖**平台、账号域与联系人**(`crab contacts …`);邮箱见
|
|
12
|
+
`crabcloud-mail` skill(`crab mail …`),
|
|
13
|
+
云盘见 `crabcloud-storage` skill(`crab storage …`);协作随后续阶段
|
|
14
|
+
提供——用户问到时如实说明,**不要猜命令**。
|
|
14
15
|
|
|
15
16
|
## 第一步:确认 CLI 可用
|
|
16
17
|
|
|
@@ -33,6 +34,9 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
|
|
|
33
34
|
| 撤销某个令牌(高危,先确认) | `crab tokens revoke <id>` |
|
|
34
35
|
| 重新授权 / 调整 scope | `crab login --scopes mail`(组名整组授权;也可单 scope 如 `mail.read`。浏览器确认,你只发起) |
|
|
35
36
|
| 登出并吊销当前令牌 | `crab logout` |
|
|
37
|
+
| 联系人:列出 / 搜索 | `crab contacts list [query]`(`--offset`/`--limit` 分页,输出含「共 N 条」) |
|
|
38
|
+
| 联系人:添加 / 删除 | `crab contacts add <address> [--name 名] [--note 备注]` / `crab contacts rm <id|address>` |
|
|
39
|
+
| 我的邀请码、邀请链接与受邀名单 | `crab contacts invite [--reset]` / `crab contacts invitees` |
|
|
36
40
|
| 管理登录设备 / 会话、修改密码 | 网页个人中心(crabcloud.cc/account)——CLI 有意不开放 |
|
|
37
41
|
|
|
38
42
|
拿不准令牌 id 时先 `crab tokens list` 核对;`revoke` 只接受列表里的 id。
|
|
@@ -64,9 +68,11 @@ Crab Cloud 是用户的个人云底座:账号、订阅与积分是平台层,
|
|
|
64
68
|
不要怂恿用户一次性给全量 scope。`--scopes` 支持组名(`mail` = 读写搜删四项
|
|
65
69
|
整组)与单个 scope 混用;组目录见 `crab help`,可用组:account / mail /
|
|
66
70
|
storage(云盘,含素材与附件)/ collab / contacts。
|
|
67
|
-
-
|
|
68
|
-
`crab
|
|
69
|
-
|
|
71
|
+
- 如实转述能力边界:协作尚未上线——相关请求如实说明,引导用户
|
|
72
|
+
用网页(crabcloud.cc),不要编造命令或输出;联系人已上线,用 `crab contacts`
|
|
73
|
+
(list / add / rm / invite / invitees;scope contacts.read/write,程序消费加
|
|
74
|
+
`--json`)处理;邮箱请求转交 `crabcloud-mail`
|
|
75
|
+
skill,云盘请求转交 `crabcloud-storage` skill。
|
|
70
76
|
- 账号安全操作(修改密码、登录设备/会话的查看与吊销)**有意不开放给 agent
|
|
71
77
|
通道**:改密对令牌持有者是账号接管面,会话属于"人的浏览器会话"。用户让
|
|
72
78
|
agent 做这些时,如实说明并引导到网页个人中心(crabcloud.cc/account)操作,
|
|
@@ -6,8 +6,9 @@ description: Read, search and send the user's Crab Cloud platform email (usernam
|
|
|
6
6
|
# Crab Cloud 邮箱(crab mail)
|
|
7
7
|
|
|
8
8
|
用户的 Crab Cloud 账号自带平台邮箱 `username@crabcloud.cc`(用户名即地址本地部分)。
|
|
9
|
-
本 skill 覆盖邮箱域的 agent 通道;账号/令牌管理见 `crabcloud` skill
|
|
10
|
-
|
|
9
|
+
本 skill 覆盖邮箱域的 agent 通道;账号/令牌管理见 `crabcloud` skill。云盘见
|
|
10
|
+
`crabcloud-storage` skill(`crab storage …`),协作尚未上线——用户问到时如实说明,
|
|
11
|
+
**不要猜命令**。
|
|
11
12
|
|
|
12
13
|
## 第一步:确认可用
|
|
13
14
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Crab Cloud 云盘(crab storage)
|
|
7
|
+
|
|
8
|
+
云盘是用户的统一存储:云盘上传 / 素材库 / 邮件附件三来源共用一本 2 GB 容量账;
|
|
9
|
+
属性优先、位置无关——分类(至多一个)+ 标签(AND 检索)代替文件夹。本 skill
|
|
10
|
+
覆盖云盘域的 agent 通道;账号/令牌管理见 `crabcloud` skill,邮箱见
|
|
11
|
+
`crabcloud-mail` skill。
|
|
12
|
+
|
|
13
|
+
## 第一步:确认可用
|
|
14
|
+
|
|
15
|
+
- `crab --version` 确认已安装;未登录(退出码 3)引导用户 `crab login`。
|
|
16
|
+
- 动作需要对应 scope:列取/下载 `storage.read`,上传/改名/标签/分类/公开
|
|
17
|
+
`storage.write`,移回收站/恢复/彻底删除 `storage.delete`。退出码 4 = scope
|
|
18
|
+
不足:如实转述缺哪个,引导 `crab login --scopes storage`(组名 = 三项整组)
|
|
19
|
+
重新授权(你只发起,不代批)。
|
|
20
|
+
- 退出码 7 = `storage.quotaExceeded`(统一容量账满,2 GB):告知用户,可建议
|
|
21
|
+
清理回收站或大文件(`storage quota` 看分项),不要自动重试。
|
|
22
|
+
|
|
23
|
+
## 意图 → 命令映射
|
|
24
|
+
|
|
25
|
+
| 用户意图 | 命令 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| 云盘里有什么 | `crab storage ls`(按时间倒序,默认 30 条) |
|
|
28
|
+
| 找文件 | `crab storage ls --q 关键字`(文件名匹配;可叠 `--source files\|media\|mail`、`--type doc\|image\|video\|archive\|code`、`--tag a,b`、`--shared`、`--recent`) |
|
|
29
|
+
| 看回收站 | `crab storage ls --trash` |
|
|
30
|
+
| 上传文件 | `crab storage upload <路径...>`(多文件串行;直传优先,自动回退中转) |
|
|
31
|
+
| 下载文件 | `crab storage download <id\|文件名> [-o 输出路径]`(默认按原文件名存当前目录) |
|
|
32
|
+
| 改名 | `crab storage rename <id\|文件名> <新名称>` |
|
|
33
|
+
| 加/删标签 | `crab storage tag <id\|文件名> --add a,b [--remove c,d]` |
|
|
34
|
+
| 归类 / 建分类 | `crab storage category <id\|文件名> <categoryId\|none>`;`crab storage mkcat <名称>`;分类 id 见 `crab storage categories` |
|
|
35
|
+
| 公开分享 | `crab storage publish <id\|文件名>`(打印公开直链;收回用 `unpublish`) |
|
|
36
|
+
| 删除 | `crab storage rm <id\|文件名>`(进回收站,7 天后自动清理)→ `restore` 恢复 / `purge` 彻底删除 |
|
|
37
|
+
| 看水位 | `crab storage quota`(三来源分项 + 回收站占用 + 对象计数) |
|
|
38
|
+
|
|
39
|
+
`<id|文件名>`:文件名全库精确唯一命中时自动解析为 id(回收站内同样可解析,
|
|
40
|
+
restore/purge 直接用文件名即可);命中多个或查不到时按字面当 id 用——拿不准
|
|
41
|
+
先 `crab storage ls --q <关键字>` 核对再操作。
|
|
42
|
+
|
|
43
|
+
## 机器可读输出
|
|
44
|
+
|
|
45
|
+
程序化消费**始终加 `--json`**:
|
|
46
|
+
|
|
47
|
+
- `storage ls --json` → `{ objects: [...], nextOffset }`,条目含
|
|
48
|
+
`id / source / filename / mimeType / sizeBytes / categoryId / tags / isPublic / createdAt`;
|
|
49
|
+
- `storage quota --json` → `{ usedBytes, quotaBytes, bySource: { files, media, mail }, trashBytes, counts }`;
|
|
50
|
+
- `upload` / `categories` / `tags` 的 `--json` 同构(单对象 / 分类数组 / 标签数组)。
|
|
51
|
+
|
|
52
|
+
## 纪律
|
|
53
|
+
|
|
54
|
+
- **publish 是对外公开**:公开后任何持链接者可访问,先向用户复述对象与后果,
|
|
55
|
+
确认后再执行;收回用 `unpublish`(已缓存浏览器至多 5 分钟内失效)。
|
|
56
|
+
- **purge 是彻底删除**,不可恢复;用户没明说「彻底删除」时一律用 `rm`
|
|
57
|
+
(回收站保留 7 天可找回)。
|
|
58
|
+
- 邮件附件与用户上传同账:水位告急时用 `quota` 分项定位来源再建议清理。
|
|
59
|
+
- 公开直链的呈现方式由服务端策略决定(图片/视频/音频/PDF/纯文本内联,SVG 与
|
|
60
|
+
其他类型强制下载),CLI 侧没有、也不需要有开关。
|
package/package.json
CHANGED