gaoding-cli 1.0.0-alpha.21 → 1.0.0-alpha.22
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/contracts/operations/dam.copy/input.schema.json +19 -0
- package/contracts/operations/dam.copy/output.schema.json +19 -0
- package/contracts/operations/dam.download/input.schema.json +14 -0
- package/contracts/operations/dam.download/output.schema.json +25 -0
- package/contracts/operations/dam.folder.list/input.schema.json +15 -0
- package/contracts/operations/dam.folder.list/output.schema.json +37 -0
- package/contracts/operations/dam.list/output.schema.json +3 -3
- package/contracts/operations/dam.move/input.schema.json +19 -0
- package/contracts/operations/dam.move/output.schema.json +18 -0
- package/contracts/operations/dam.recycle.list/input.schema.json +13 -0
- package/contracts/operations/dam.recycle.list/output.schema.json +34 -0
- package/contracts/operations/dam.recycle.restore/input.schema.json +14 -0
- package/contracts/operations/dam.recycle.restore/output.schema.json +6 -0
- package/contracts/operations/dam.rename/input.schema.json +18 -0
- package/contracts/operations/dam.rename/output.schema.json +18 -0
- package/contracts/operations/dam.repository.list/input.schema.json +8 -0
- package/contracts/operations/dam.repository.list/output.schema.json +52 -0
- package/contracts/operations/dam.search/output.schema.json +3 -3
- package/contracts/operations/dam.tag.list/input.schema.json +12 -0
- package/contracts/operations/dam.tag.list/output.schema.json +26 -0
- package/dist/src/bootstrap/create-runtime.js +24 -0
- package/dist/src/bootstrap/validators.js +39 -0
- package/dist/src/cli/dam-commands.js +248 -1
- package/dist/src/cli/errors.js +22 -2
- package/dist/src/cli/presenter.js +12 -0
- package/dist/src/features/dam/dam-api-adapter.js +369 -28
- package/dist/src/features/dam/file-downloader.js +242 -0
- package/dist/src/features/dam/operation-executor.js +72 -0
- package/dist/src/features/dam/repository-catalog.js +127 -0
- package/dist/src/features/dam/use-cases.js +323 -11
- package/dist/src/features/editor/bridge-server.js +6 -1
- package/dist/src/platform/signature.js +2 -1
- package/dist/src/platform/signed-http-transport.js +9 -1
- package/package.json +1 -1
- package/skills/gd-cli/SKILL.md +1 -1
- package/skills/gd-cli/references/dam.md +45 -13
- package/skills/gd-cli/references/editor.md +3 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export class DamOperationFailedError extends Error {
|
|
2
|
+
operation;
|
|
3
|
+
constructor(operation) {
|
|
4
|
+
super("DAM 操作执行失败。");
|
|
5
|
+
this.name = "DamOperationFailedError";
|
|
6
|
+
this.operation = operation;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class DamOperationUnconfirmedError extends Error {
|
|
10
|
+
operation;
|
|
11
|
+
constructor(operation) {
|
|
12
|
+
super("DAM 操作已提交,但未能确认最终结果。");
|
|
13
|
+
this.name = "DamOperationUnconfirmedError";
|
|
14
|
+
this.operation = operation;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function copiedEntryId(resultIds) {
|
|
18
|
+
const unique = [...new Set((resultIds ?? []).filter((value) => value.trim() !== ""))];
|
|
19
|
+
return unique.length === 1 ? unique[0] : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function createDamOperationExecutor(dependencies) {
|
|
22
|
+
return {
|
|
23
|
+
async execute(input) {
|
|
24
|
+
input.signal.throwIfAborted();
|
|
25
|
+
const startedAt = dependencies.now();
|
|
26
|
+
const submission = await input.submit();
|
|
27
|
+
input.signal.throwIfAborted();
|
|
28
|
+
const taskId = submission.task_id;
|
|
29
|
+
if (taskId === undefined) {
|
|
30
|
+
const copied = input.operation === "copy"
|
|
31
|
+
? copiedEntryId(submission.result_ids)
|
|
32
|
+
: undefined;
|
|
33
|
+
return copied === undefined ? {} : { copied_entry_id: copied };
|
|
34
|
+
}
|
|
35
|
+
dependencies.annotate({ task_id: taskId });
|
|
36
|
+
let delayMilliseconds = 1_000;
|
|
37
|
+
const deadline = startedAt + 180_000;
|
|
38
|
+
while (true) {
|
|
39
|
+
input.signal.throwIfAborted();
|
|
40
|
+
if (dependencies.now() >= deadline) {
|
|
41
|
+
throw new DamOperationUnconfirmedError(input.operation);
|
|
42
|
+
}
|
|
43
|
+
const task = await input.getTask(taskId, input.signal);
|
|
44
|
+
input.signal.throwIfAborted();
|
|
45
|
+
if (task === null)
|
|
46
|
+
throw new DamOperationUnconfirmedError(input.operation);
|
|
47
|
+
if (task.status === 2) {
|
|
48
|
+
if (task.fail_count !== null && task.fail_count > 0) {
|
|
49
|
+
throw new DamOperationFailedError(input.operation);
|
|
50
|
+
}
|
|
51
|
+
if (input.operation !== "copy" || input.inspectResult === undefined)
|
|
52
|
+
return {};
|
|
53
|
+
const result = await input.inspectResult(taskId, input.signal);
|
|
54
|
+
input.signal.throwIfAborted();
|
|
55
|
+
const copied = copiedEntryId(result.result_ids);
|
|
56
|
+
return copied === undefined ? {} : { copied_entry_id: copied };
|
|
57
|
+
}
|
|
58
|
+
if (task.status === 3)
|
|
59
|
+
throw new DamOperationFailedError(input.operation);
|
|
60
|
+
if (task.status !== 0 && task.status !== 1) {
|
|
61
|
+
throw new DamOperationUnconfirmedError(input.operation);
|
|
62
|
+
}
|
|
63
|
+
const remaining = deadline - dependencies.now();
|
|
64
|
+
if (remaining <= 0)
|
|
65
|
+
throw new DamOperationUnconfirmedError(input.operation);
|
|
66
|
+
await dependencies.sleep(Math.min(delayMilliseconds, remaining), input.signal);
|
|
67
|
+
input.signal.throwIfAborted();
|
|
68
|
+
delayMilliseconds = Math.min(delayMilliseconds + 500, 3_000);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { RemoteProtocolError } from "../../platform/remote-protocol.js";
|
|
2
|
+
const permissions = new Set([
|
|
3
|
+
"OWNER",
|
|
4
|
+
"MANAGE",
|
|
5
|
+
"EDIT",
|
|
6
|
+
"USE",
|
|
7
|
+
"READ"
|
|
8
|
+
]);
|
|
9
|
+
function record(value) {
|
|
10
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: undefined;
|
|
13
|
+
}
|
|
14
|
+
function rows(value) {
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return value;
|
|
17
|
+
const nested = record(value)?.data;
|
|
18
|
+
if (Array.isArray(nested))
|
|
19
|
+
return nested;
|
|
20
|
+
throw new RemoteProtocolError();
|
|
21
|
+
}
|
|
22
|
+
function nonblank(value) {
|
|
23
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
function permission(value) {
|
|
26
|
+
return typeof value === "string"
|
|
27
|
+
&& permissions.has(value)
|
|
28
|
+
? value
|
|
29
|
+
: undefined;
|
|
30
|
+
}
|
|
31
|
+
function privateRepositories(value) {
|
|
32
|
+
return rows(value).flatMap((candidate) => {
|
|
33
|
+
const item = record(candidate);
|
|
34
|
+
const repositoryId = nonblank(item?.repository_id);
|
|
35
|
+
const name = nonblank(item?.name) ?? nonblank(item?.title);
|
|
36
|
+
const type = item?.type === 1
|
|
37
|
+
? "personal"
|
|
38
|
+
: item?.type === 6
|
|
39
|
+
? "draft"
|
|
40
|
+
: undefined;
|
|
41
|
+
if (repositoryId === undefined || name === undefined || type === undefined)
|
|
42
|
+
return [];
|
|
43
|
+
return [{ repository_id: repositoryId, name, type, is_default: false }];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function teamRepositories(value) {
|
|
47
|
+
return rows(value).flatMap((candidate) => {
|
|
48
|
+
const item = record(candidate);
|
|
49
|
+
const repositoryId = nonblank(item?.repository_id);
|
|
50
|
+
const name = nonblank(item?.name) ?? nonblank(item?.title);
|
|
51
|
+
if (repositoryId === undefined || name === undefined)
|
|
52
|
+
return [];
|
|
53
|
+
const permissionCode = permission(item?.permission_code);
|
|
54
|
+
return [{
|
|
55
|
+
repository_id: repositoryId,
|
|
56
|
+
name,
|
|
57
|
+
type: "team",
|
|
58
|
+
...(permissionCode === undefined ? {} : { permission_code: permissionCode }),
|
|
59
|
+
is_default: false
|
|
60
|
+
}];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function enterpriseRepositories(value) {
|
|
64
|
+
return rows(value).flatMap((candidate) => {
|
|
65
|
+
const item = record(candidate);
|
|
66
|
+
const repositoryId = nonblank(item?.id);
|
|
67
|
+
const name = nonblank(item?.name) ?? nonblank(item?.title);
|
|
68
|
+
if (repositoryId === undefined || name === undefined)
|
|
69
|
+
return [];
|
|
70
|
+
const permissionCode = permission(item?.permission_code);
|
|
71
|
+
return [{
|
|
72
|
+
repository_id: repositoryId,
|
|
73
|
+
name,
|
|
74
|
+
type: "enterprise",
|
|
75
|
+
...(permissionCode === undefined ? {} : { permission_code: permissionCode }),
|
|
76
|
+
is_default: false
|
|
77
|
+
}];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export function normalizeRepositoryCatalog(privateRows, teamRows, enterpriseRows) {
|
|
81
|
+
const merged = [
|
|
82
|
+
...privateRepositories(privateRows),
|
|
83
|
+
...teamRepositories(teamRows),
|
|
84
|
+
...enterpriseRepositories(enterpriseRows)
|
|
85
|
+
];
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
const unique = merged.filter((repository) => {
|
|
88
|
+
if (seen.has(repository.repository_id))
|
|
89
|
+
return false;
|
|
90
|
+
seen.add(repository.repository_id);
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
const defaultIndex = unique.findIndex((repository) => repository.type === "personal");
|
|
94
|
+
const selectedIndex = defaultIndex === -1 && unique.length > 0 ? 0 : defaultIndex;
|
|
95
|
+
return unique.map((repository, index) => ({
|
|
96
|
+
...repository,
|
|
97
|
+
is_default: index === selectedIndex
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
function requestContext(input) {
|
|
101
|
+
return {
|
|
102
|
+
credential: input.access.credential,
|
|
103
|
+
organizationId: input.access.organizationId,
|
|
104
|
+
signal: input.signal
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function createDamRepositoryCatalog(dependencies) {
|
|
108
|
+
return {
|
|
109
|
+
async list(input) {
|
|
110
|
+
const [privateRows, teamRows, enterpriseRows] = await Promise.all([
|
|
111
|
+
dependencies.transport.getJson({
|
|
112
|
+
path: "/tb-dam/repositories",
|
|
113
|
+
...requestContext(input)
|
|
114
|
+
}),
|
|
115
|
+
dependencies.transport.getJson({
|
|
116
|
+
path: "/tb-dam/teams",
|
|
117
|
+
...requestContext(input)
|
|
118
|
+
}),
|
|
119
|
+
dependencies.transport.getJson({
|
|
120
|
+
path: "/dam/org-repositories",
|
|
121
|
+
...requestContext(input)
|
|
122
|
+
})
|
|
123
|
+
]);
|
|
124
|
+
return normalizeRepositoryCatalog(privateRows, teamRows, enterpriseRows);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import {
|
|
3
|
+
import { DamOperationUnconfirmedError } from "./operation-executor.js";
|
|
4
4
|
export class DamInputError extends Error {
|
|
5
5
|
constructor() {
|
|
6
6
|
super("DAM 输入不可用。");
|
|
7
7
|
this.name = "DamInputError";
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
export class DamRepositoryNotFoundError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("当前组织没有可用的 DAM 资源库。");
|
|
13
|
+
this.name = "DamRepositoryNotFoundError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export { DamOperationUnconfirmedError } from "./operation-executor.js";
|
|
10
17
|
function invalid() {
|
|
11
18
|
throw new DamInputError();
|
|
12
19
|
}
|
|
@@ -17,6 +24,14 @@ function assertPageSize(value) {
|
|
|
17
24
|
if (value !== undefined && (!Number.isInteger(value) || value < 1 || value > 100))
|
|
18
25
|
invalid();
|
|
19
26
|
}
|
|
27
|
+
function assertPage(value) {
|
|
28
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 1))
|
|
29
|
+
invalid();
|
|
30
|
+
}
|
|
31
|
+
function assertOptionalNonblank(value) {
|
|
32
|
+
if (value !== undefined && !nonblank(value))
|
|
33
|
+
invalid();
|
|
34
|
+
}
|
|
20
35
|
function assertAssetId(value) {
|
|
21
36
|
if (value.trim() === "" || /^https?:\/\//iu.test(value))
|
|
22
37
|
invalid();
|
|
@@ -42,23 +57,139 @@ function searchRequest(input, keyword, repositoryIds) {
|
|
|
42
57
|
}
|
|
43
58
|
export function createDamUseCases(dependencies) {
|
|
44
59
|
const validate = dependencies.validate;
|
|
60
|
+
async function entryTitle(input) {
|
|
61
|
+
if (input.kind === "asset") {
|
|
62
|
+
return (await dependencies.api.getAsset({
|
|
63
|
+
assetId: input.entryId,
|
|
64
|
+
access: input.access,
|
|
65
|
+
signal: input.signal
|
|
66
|
+
})).title;
|
|
67
|
+
}
|
|
68
|
+
return (await dependencies.api.getFolder({
|
|
69
|
+
folderId: input.entryId,
|
|
70
|
+
repositoryId: input.repositoryId,
|
|
71
|
+
access: input.access,
|
|
72
|
+
signal: input.signal
|
|
73
|
+
})).title;
|
|
74
|
+
}
|
|
75
|
+
async function entryLocation(input) {
|
|
76
|
+
if (input.kind === "asset") {
|
|
77
|
+
const detail = await dependencies.api.getAsset({
|
|
78
|
+
assetId: input.entryId,
|
|
79
|
+
access: input.access,
|
|
80
|
+
signal: input.signal
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
...(detail.repository_id === undefined ? {} : { repositoryId: detail.repository_id }),
|
|
84
|
+
...(detail.folder_id === undefined ? {} : { folderId: detail.folder_id })
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const detail = await dependencies.api.getFolder({
|
|
88
|
+
folderId: input.entryId,
|
|
89
|
+
repositoryId: input.repositoryId,
|
|
90
|
+
access: input.access,
|
|
91
|
+
signal: input.signal
|
|
92
|
+
});
|
|
93
|
+
return { repositoryId: detail.repository_id, folderId: detail.folder_id };
|
|
94
|
+
}
|
|
95
|
+
function assertEntryOperationInput(input) {
|
|
96
|
+
assertAssetId(input.entry_id);
|
|
97
|
+
if (input.kind !== "asset" && input.kind !== "folder")
|
|
98
|
+
invalid();
|
|
99
|
+
if (!nonblank(input.targetFolderId))
|
|
100
|
+
invalid();
|
|
101
|
+
assertOptionalNonblank(input.repositoryId);
|
|
102
|
+
assertOptionalNonblank(input.targetRepositoryId);
|
|
103
|
+
if (input.kind === "folder" && input.entry_id === input.targetFolderId)
|
|
104
|
+
invalid();
|
|
105
|
+
}
|
|
106
|
+
async function entryOperation(input) {
|
|
107
|
+
assertEntryOperationInput(input.request);
|
|
108
|
+
const [sourceRepository] = await dependencies.telemetry.stage("repository", () => (resolveRepositories({
|
|
109
|
+
...(input.request.repositoryId === undefined
|
|
110
|
+
? {}
|
|
111
|
+
: { repositoryId: input.request.repositoryId })
|
|
112
|
+
}, input.access, input.signal)));
|
|
113
|
+
if (sourceRepository === undefined)
|
|
114
|
+
throw new DamRepositoryNotFoundError();
|
|
115
|
+
const targetRepositoryId = input.request.targetRepositoryId ?? sourceRepository.id;
|
|
116
|
+
const locationRequest = {
|
|
117
|
+
entryId: input.request.entry_id,
|
|
118
|
+
kind: input.request.kind,
|
|
119
|
+
repositoryId: sourceRepository.id,
|
|
120
|
+
access: input.access,
|
|
121
|
+
signal: input.signal
|
|
122
|
+
};
|
|
123
|
+
const current = await dependencies.telemetry.stage("resolve", () => (entryLocation(locationRequest)));
|
|
124
|
+
if (input.operation === "move"
|
|
125
|
+
&& current.repositoryId === targetRepositoryId
|
|
126
|
+
&& current.folderId === input.request.targetFolderId)
|
|
127
|
+
invalid();
|
|
128
|
+
const execution = await dependencies.telemetry.stage("wait", () => (dependencies.operations.execute({
|
|
129
|
+
operation: input.operation,
|
|
130
|
+
submit: () => dependencies.telemetry.stage("submit", () => (dependencies.api.submitEntryOperation({
|
|
131
|
+
entryId: input.request.entry_id,
|
|
132
|
+
kind: input.request.kind,
|
|
133
|
+
operation: input.operation,
|
|
134
|
+
sourceRepositoryId: sourceRepository.id,
|
|
135
|
+
targetRepositoryId,
|
|
136
|
+
targetFolderId: input.request.targetFolderId,
|
|
137
|
+
access: input.access,
|
|
138
|
+
signal: input.signal
|
|
139
|
+
}))),
|
|
140
|
+
getTask: (taskId, signal) => dependencies.api.getOperationTask({
|
|
141
|
+
taskId,
|
|
142
|
+
access: input.access,
|
|
143
|
+
signal
|
|
144
|
+
}),
|
|
145
|
+
...(input.operation === "copy"
|
|
146
|
+
? {
|
|
147
|
+
inspectResult: (taskId, signal) => (dependencies.api.getOperationChildren({
|
|
148
|
+
taskId,
|
|
149
|
+
access: input.access,
|
|
150
|
+
signal
|
|
151
|
+
}))
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
signal: input.signal
|
|
155
|
+
})));
|
|
156
|
+
return {
|
|
157
|
+
sourceRepositoryId: sourceRepository.id,
|
|
158
|
+
targetRepositoryId,
|
|
159
|
+
targetFolderId: input.request.targetFolderId,
|
|
160
|
+
...(execution.copied_entry_id === undefined
|
|
161
|
+
? {}
|
|
162
|
+
: { copiedEntryId: execution.copied_entry_id })
|
|
163
|
+
};
|
|
164
|
+
}
|
|
45
165
|
async function resolveRepositories(input, access, signal) {
|
|
46
166
|
assertRepositorySelection(input);
|
|
47
167
|
if (input.repositoryId !== undefined)
|
|
48
168
|
return [{ id: input.repositoryId }];
|
|
49
169
|
signal.throwIfAborted();
|
|
50
|
-
const available = await dependencies.
|
|
170
|
+
const available = await dependencies.repositories.list({ access, signal });
|
|
51
171
|
signal.throwIfAborted();
|
|
52
172
|
if (available.length === 0)
|
|
53
|
-
throw new
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
173
|
+
throw new DamRepositoryNotFoundError();
|
|
174
|
+
const selected = input.allRepositories === true
|
|
175
|
+
? available
|
|
176
|
+
: [available.find((repository) => repository.is_default) ?? available[0]];
|
|
177
|
+
return selected.map((repository) => ({
|
|
178
|
+
id: repository.repository_id,
|
|
179
|
+
...(repository.name === undefined ? {} : { name: repository.name })
|
|
180
|
+
}));
|
|
60
181
|
}
|
|
61
182
|
return {
|
|
183
|
+
async repositoryList({ access, signal }) {
|
|
184
|
+
signal.throwIfAborted();
|
|
185
|
+
const repositories = await dependencies.repositories.list({ access, signal });
|
|
186
|
+
signal.throwIfAborted();
|
|
187
|
+
if (repositories.length === 0)
|
|
188
|
+
throw new DamRepositoryNotFoundError();
|
|
189
|
+
const result = { repositories };
|
|
190
|
+
validate.repositoryList(result);
|
|
191
|
+
return result;
|
|
192
|
+
},
|
|
62
193
|
async list({ input, access, signal }) {
|
|
63
194
|
signal.throwIfAborted();
|
|
64
195
|
assertPageSize(input.pageSize);
|
|
@@ -91,6 +222,187 @@ export function createDamUseCases(dependencies) {
|
|
|
91
222
|
return result;
|
|
92
223
|
});
|
|
93
224
|
},
|
|
225
|
+
async folderList({ input, access, signal }) {
|
|
226
|
+
signal.throwIfAborted();
|
|
227
|
+
assertOptionalNonblank(input.parentId);
|
|
228
|
+
assertOptionalNonblank(input.query);
|
|
229
|
+
assertPage(input.page);
|
|
230
|
+
assertPageSize(input.pageSize);
|
|
231
|
+
const [repository] = await dependencies.telemetry.stage("repository", () => (resolveRepositories({
|
|
232
|
+
...(input.repositoryId === undefined ? {} : { repositoryId: input.repositoryId })
|
|
233
|
+
}, access, signal)));
|
|
234
|
+
if (repository === undefined)
|
|
235
|
+
throw new DamRepositoryNotFoundError();
|
|
236
|
+
return dependencies.telemetry.stage("request", async () => {
|
|
237
|
+
const result = await dependencies.api.listFolders({
|
|
238
|
+
repositoryId: repository.id,
|
|
239
|
+
parentId: input.parentId ?? "0",
|
|
240
|
+
page: input.page ?? 1,
|
|
241
|
+
pageSize: input.pageSize ?? 20,
|
|
242
|
+
...(input.query === undefined ? {} : { query: input.query }),
|
|
243
|
+
access,
|
|
244
|
+
signal
|
|
245
|
+
});
|
|
246
|
+
signal.throwIfAborted();
|
|
247
|
+
validate.folderList(result);
|
|
248
|
+
return result;
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
async tagList({ input, access, signal }) {
|
|
252
|
+
signal.throwIfAborted();
|
|
253
|
+
assertOptionalNonblank(input.query);
|
|
254
|
+
const [repository] = await dependencies.telemetry.stage("repository", () => (resolveRepositories({
|
|
255
|
+
...(input.repositoryId === undefined ? {} : { repositoryId: input.repositoryId })
|
|
256
|
+
}, access, signal)));
|
|
257
|
+
if (repository === undefined)
|
|
258
|
+
throw new DamRepositoryNotFoundError();
|
|
259
|
+
return dependencies.telemetry.stage("request", async () => {
|
|
260
|
+
const result = await dependencies.api.listTags({
|
|
261
|
+
repositoryId: repository.id,
|
|
262
|
+
...(input.query === undefined ? {} : { query: input.query }),
|
|
263
|
+
access,
|
|
264
|
+
signal
|
|
265
|
+
});
|
|
266
|
+
signal.throwIfAborted();
|
|
267
|
+
validate.tagList(result);
|
|
268
|
+
return result;
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
async download({ input, access, signal }) {
|
|
272
|
+
signal.throwIfAborted();
|
|
273
|
+
assertAssetId(input.asset_id);
|
|
274
|
+
assertOptionalNonblank(input.outputDir);
|
|
275
|
+
const [repository] = await dependencies.telemetry.stage("repository", () => (resolveRepositories({
|
|
276
|
+
...(input.repositoryId === undefined ? {} : { repositoryId: input.repositoryId })
|
|
277
|
+
}, access, signal)));
|
|
278
|
+
if (repository === undefined)
|
|
279
|
+
throw new DamRepositoryNotFoundError();
|
|
280
|
+
const detail = await dependencies.telemetry.stage("resolve", () => (dependencies.api.getAsset({ assetId: input.asset_id, access, signal })));
|
|
281
|
+
signal.throwIfAborted();
|
|
282
|
+
const urls = await dependencies.telemetry.stage("download", () => (dependencies.api.getDownloadUrls({
|
|
283
|
+
assetId: input.asset_id,
|
|
284
|
+
repositoryId: repository.id,
|
|
285
|
+
access,
|
|
286
|
+
signal
|
|
287
|
+
})));
|
|
288
|
+
signal.throwIfAborted();
|
|
289
|
+
const files = await dependencies.telemetry.stage("persist", () => (dependencies.downloader.download({
|
|
290
|
+
urls,
|
|
291
|
+
...(detail.title === undefined ? {} : { fallbackTitle: detail.title }),
|
|
292
|
+
...(detail.format === undefined ? {} : { fallbackFormat: detail.format }),
|
|
293
|
+
outputDirectory: input.outputDir ?? ".",
|
|
294
|
+
signal
|
|
295
|
+
})));
|
|
296
|
+
signal.throwIfAborted();
|
|
297
|
+
const result = { asset_id: input.asset_id, files };
|
|
298
|
+
validate.download(result);
|
|
299
|
+
return result;
|
|
300
|
+
},
|
|
301
|
+
async rename({ input, access, signal }) {
|
|
302
|
+
signal.throwIfAborted();
|
|
303
|
+
assertAssetId(input.entry_id);
|
|
304
|
+
if (input.kind !== "asset" && input.kind !== "folder")
|
|
305
|
+
invalid();
|
|
306
|
+
if (!nonblank(input.title) || input.title.length > 100)
|
|
307
|
+
invalid();
|
|
308
|
+
const [repository] = await dependencies.telemetry.stage("repository", () => (resolveRepositories({
|
|
309
|
+
...(input.repositoryId === undefined ? {} : { repositoryId: input.repositoryId })
|
|
310
|
+
}, access, signal)));
|
|
311
|
+
if (repository === undefined)
|
|
312
|
+
throw new DamRepositoryNotFoundError();
|
|
313
|
+
const request = {
|
|
314
|
+
entryId: input.entry_id,
|
|
315
|
+
kind: input.kind,
|
|
316
|
+
repositoryId: repository.id,
|
|
317
|
+
access,
|
|
318
|
+
signal
|
|
319
|
+
};
|
|
320
|
+
const currentTitle = await dependencies.telemetry.stage("rename", async () => {
|
|
321
|
+
const title = await entryTitle(request);
|
|
322
|
+
signal.throwIfAborted();
|
|
323
|
+
if (title?.trim() !== input.title.trim()) {
|
|
324
|
+
await dependencies.api.renameEntry({
|
|
325
|
+
...request,
|
|
326
|
+
repositoryId: repository.id,
|
|
327
|
+
title: input.title
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return title;
|
|
331
|
+
});
|
|
332
|
+
if (currentTitle?.trim() === input.title.trim()) {
|
|
333
|
+
const result = {
|
|
334
|
+
entry_id: input.entry_id,
|
|
335
|
+
kind: input.kind,
|
|
336
|
+
repository_id: repository.id,
|
|
337
|
+
title: currentTitle
|
|
338
|
+
};
|
|
339
|
+
validate.rename(result);
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
await dependencies.telemetry.stage("verify", async () => {
|
|
343
|
+
for (let attempt = 0; attempt <= 30; attempt += 1) {
|
|
344
|
+
signal.throwIfAborted();
|
|
345
|
+
if (await entryTitle(request) === input.title)
|
|
346
|
+
return;
|
|
347
|
+
if (attempt < 30)
|
|
348
|
+
await dependencies.sleep(2_000, signal);
|
|
349
|
+
}
|
|
350
|
+
throw new DamOperationUnconfirmedError("rename");
|
|
351
|
+
});
|
|
352
|
+
const result = {
|
|
353
|
+
entry_id: input.entry_id,
|
|
354
|
+
kind: input.kind,
|
|
355
|
+
repository_id: repository.id,
|
|
356
|
+
title: input.title
|
|
357
|
+
};
|
|
358
|
+
validate.rename(result);
|
|
359
|
+
return result;
|
|
360
|
+
},
|
|
361
|
+
async move({ input, access, signal }) {
|
|
362
|
+
signal.throwIfAborted();
|
|
363
|
+
const operation = await entryOperation({ operation: "move", request: input, access, signal });
|
|
364
|
+
await dependencies.telemetry.stage("verify", async () => {
|
|
365
|
+
for (let attempt = 0; attempt <= 30; attempt += 1) {
|
|
366
|
+
signal.throwIfAborted();
|
|
367
|
+
const location = await entryLocation({
|
|
368
|
+
entryId: input.entry_id,
|
|
369
|
+
kind: input.kind,
|
|
370
|
+
repositoryId: operation.targetRepositoryId,
|
|
371
|
+
access,
|
|
372
|
+
signal
|
|
373
|
+
});
|
|
374
|
+
if (location.repositoryId === operation.targetRepositoryId
|
|
375
|
+
&& location.folderId === operation.targetFolderId)
|
|
376
|
+
return;
|
|
377
|
+
if (attempt < 30)
|
|
378
|
+
await dependencies.sleep(2_000, signal);
|
|
379
|
+
}
|
|
380
|
+
throw new DamOperationUnconfirmedError("move");
|
|
381
|
+
});
|
|
382
|
+
const result = {
|
|
383
|
+
entry_id: input.entry_id,
|
|
384
|
+
kind: input.kind,
|
|
385
|
+
repository_id: operation.targetRepositoryId,
|
|
386
|
+
folder_id: operation.targetFolderId
|
|
387
|
+
};
|
|
388
|
+
await dependencies.telemetry.stage("output", () => { validate.move(result); });
|
|
389
|
+
return result;
|
|
390
|
+
},
|
|
391
|
+
async copy({ input, access, signal }) {
|
|
392
|
+
signal.throwIfAborted();
|
|
393
|
+
const operation = await entryOperation({ operation: "copy", request: input, access, signal });
|
|
394
|
+
const result = {
|
|
395
|
+
source_entry_id: input.entry_id,
|
|
396
|
+
kind: input.kind,
|
|
397
|
+
repository_id: operation.targetRepositoryId,
|
|
398
|
+
folder_id: operation.targetFolderId,
|
|
399
|
+
...(operation.copiedEntryId === undefined
|
|
400
|
+
? {}
|
|
401
|
+
: { copied_entry_id: operation.copiedEntryId })
|
|
402
|
+
};
|
|
403
|
+
await dependencies.telemetry.stage("output", () => { validate.copy(result); });
|
|
404
|
+
return result;
|
|
405
|
+
},
|
|
94
406
|
async get({ input, access, signal }) {
|
|
95
407
|
signal.throwIfAborted();
|
|
96
408
|
assertAssetId(input.asset_id);
|
|
@@ -120,7 +432,7 @@ export function createDamUseCases(dependencies) {
|
|
|
120
432
|
...(input.repositoryId === undefined ? {} : { repositoryId: input.repositoryId })
|
|
121
433
|
}, access, signal)));
|
|
122
434
|
if (repository === undefined)
|
|
123
|
-
throw new
|
|
435
|
+
throw new DamRepositoryNotFoundError();
|
|
124
436
|
const result = await dependencies.uploader.upload({
|
|
125
437
|
input,
|
|
126
438
|
file,
|
|
@@ -139,7 +451,7 @@ export function createDamUseCases(dependencies) {
|
|
|
139
451
|
...(input.repositoryId === undefined ? {} : { repositoryId: input.repositoryId })
|
|
140
452
|
}, access, signal)));
|
|
141
453
|
if (repository === undefined)
|
|
142
|
-
throw new
|
|
454
|
+
throw new DamRepositoryNotFoundError();
|
|
143
455
|
const request = {
|
|
144
456
|
repositoryId: repository.id,
|
|
145
457
|
assetId: input.asset_id,
|
|
@@ -3,11 +3,14 @@ import { createServer } from "node:http";
|
|
|
3
3
|
import { WebSocket, WebSocketServer } from "ws";
|
|
4
4
|
import { EDITOR_PROTOCOL_VERSION } from "./protocol.js";
|
|
5
5
|
const DEFAULT_RPC_TIMEOUT_MS = 60_000;
|
|
6
|
+
const DEFAULT_SAVE_RPC_TIMEOUT_MS = 5 * 60_000 + 30_000;
|
|
6
7
|
export async function startEditorBridge(options) {
|
|
7
8
|
const token = options.token ?? randomBytes(32).toString("hex");
|
|
8
9
|
const now = options.now ?? (() => new Date());
|
|
9
10
|
const nextRequestId = options.requestId ?? randomUUID;
|
|
10
|
-
const
|
|
11
|
+
const rpcTimeoutOverrideMs = options.rpcTimeoutMs === undefined
|
|
12
|
+
? undefined
|
|
13
|
+
: Math.max(0, options.rpcTimeoutMs);
|
|
11
14
|
const webSockets = new WebSocketServer({ noServer: true });
|
|
12
15
|
let pageSocket = null;
|
|
13
16
|
let pending;
|
|
@@ -146,6 +149,8 @@ export async function startEditorBridge(options) {
|
|
|
146
149
|
}
|
|
147
150
|
const requestId = nextRequestId();
|
|
148
151
|
const socket = pageSocket;
|
|
152
|
+
const rpcTimeoutMs = rpcTimeoutOverrideMs
|
|
153
|
+
?? (body.method === "save" ? DEFAULT_SAVE_RPC_TIMEOUT_MS : DEFAULT_RPC_TIMEOUT_MS);
|
|
149
154
|
const timer = setTimeout(() => {
|
|
150
155
|
const current = takePending(requestId);
|
|
151
156
|
if (!current)
|
|
@@ -3,7 +3,8 @@ export function canonicalizeQuery(entries) {
|
|
|
3
3
|
const sorted = [...entries].sort(([left], [right]) => left.localeCompare(right));
|
|
4
4
|
return new URLSearchParams(sorted.map(([key, value]) => [key, value]))
|
|
5
5
|
.toString()
|
|
6
|
-
.replaceAll("%2C", ",")
|
|
6
|
+
.replaceAll("%2C", ",")
|
|
7
|
+
.replaceAll("%3A", ":");
|
|
7
8
|
}
|
|
8
9
|
export function createSigningString(input) {
|
|
9
10
|
return `${input.method.toUpperCase()}@${input.pathname}@${input.canonicalQuery}@${input.timestamp}`;
|
|
@@ -108,13 +108,21 @@ export function createSignedHttpTransport(options) {
|
|
|
108
108
|
throw new RemoteProtocolError(evidence);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
+
async function getJsonResponse(request) {
|
|
112
|
+
const response = await send("GET", request, "application/json", undefined, true);
|
|
113
|
+
return { body: await json(response), headers: response.headers };
|
|
114
|
+
}
|
|
111
115
|
return {
|
|
112
116
|
async getJson(request) {
|
|
113
|
-
return
|
|
117
|
+
return (await getJsonResponse(request)).body;
|
|
114
118
|
},
|
|
119
|
+
getJsonResponse,
|
|
115
120
|
async postJson(request) {
|
|
116
121
|
return json(await send("POST", request, "application/json", JSON.stringify(request.body), true));
|
|
117
122
|
},
|
|
123
|
+
async putJson(request) {
|
|
124
|
+
return json(await send("PUT", request, "application/json", JSON.stringify(request.body), true));
|
|
125
|
+
},
|
|
118
126
|
async postStream(request) {
|
|
119
127
|
const response = await send("POST", request, request.accept, JSON.stringify(request.body), false);
|
|
120
128
|
if (!response.ok) {
|
package/package.json
CHANGED
package/skills/gd-cli/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ description: Use when creating images, videos or text with GD CLI, operating the
|
|
|
9
9
|
|
|
10
10
|
- 按业务意图选择 Agent / Tool 创作路由、发现 Model 并生成图片、视频或文本:[创作](references/creation.md)
|
|
11
11
|
- 操作当前 AI+ Editor 作品:[编辑器](references/editor.md)
|
|
12
|
-
-
|
|
12
|
+
- 发现资源库、文件夹与标签,查询、上传、下载或单条目管理 DAM 资源:[素材管理](references/dam.md)
|
|
13
13
|
- 登录或选择组织:[登录与组织](references/auth-org.md)
|
|
14
14
|
- 处理输出、警告、失败或中断:[错误处理](references/errors.md)
|
|
15
15
|
- 更新 CLI 与随包 Agent Skill:[更新](references/update.md)
|