gaoding-cli 1.0.0-alpha.10
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/LICENSE +21 -0
- package/README.md +38 -0
- package/contracts/operations/agent.send/input.schema.json +354 -0
- package/contracts/operations/agent.send/output.schema.json +271 -0
- package/contracts/operations/auth.status/input.schema.json +7 -0
- package/contracts/operations/auth.status/output.schema.json +132 -0
- package/contracts/operations/dam.delete/input.schema.json +23 -0
- package/contracts/operations/dam.delete/output.schema.json +6 -0
- package/contracts/operations/dam.get/input.schema.json +16 -0
- package/contracts/operations/dam.get/output.schema.json +46 -0
- package/contracts/operations/dam.list/input.schema.json +44 -0
- package/contracts/operations/dam.list/output.schema.json +71 -0
- package/contracts/operations/dam.search/input.schema.json +46 -0
- package/contracts/operations/dam.search/output.schema.json +71 -0
- package/contracts/operations/dam.upload/input.schema.json +31 -0
- package/contracts/operations/dam.upload/output.schema.json +46 -0
- package/contracts/operations/editor.apply/input.schema.json +134 -0
- package/contracts/operations/editor.apply/output.schema.json +15 -0
- package/contracts/operations/editor.connect/input.schema.json +15 -0
- package/contracts/operations/editor.connect/output.schema.json +17 -0
- package/contracts/operations/editor.disconnect/input.schema.json +8 -0
- package/contracts/operations/editor.disconnect/output.schema.json +14 -0
- package/contracts/operations/editor.save/input.schema.json +8 -0
- package/contracts/operations/editor.save/output.schema.json +23 -0
- package/contracts/operations/editor.screenshot/input.schema.json +8 -0
- package/contracts/operations/editor.screenshot/output.schema.json +16 -0
- package/contracts/operations/editor.snapshot/input.schema.json +8 -0
- package/contracts/operations/editor.snapshot/output.schema.json +113 -0
- package/contracts/operations/model.get/input.schema.json +18 -0
- package/contracts/operations/model.get/output.schema.json +190 -0
- package/contracts/operations/model.list/input.schema.json +15 -0
- package/contracts/operations/model.list/output.schema.json +111 -0
- package/contracts/operations/org.current/input.schema.json +7 -0
- package/contracts/operations/org.current/output.schema.json +49 -0
- package/contracts/operations/org.list/input.schema.json +7 -0
- package/contracts/operations/org.list/output.schema.json +71 -0
- package/contracts/operations/tool.call/input.schema.json +35 -0
- package/contracts/operations/tool.call/output.schema.json +129 -0
- package/contracts/operations/tool.list/input.schema.json +7 -0
- package/contracts/operations/tool.list/output.schema.json +45 -0
- package/dist/bin/gd-cli.js +38 -0
- package/dist/bin/postinstall.js +42 -0
- package/dist/src/bootstrap/create-cli.js +55 -0
- package/dist/src/bootstrap/create-runtime.js +222 -0
- package/dist/src/bootstrap/validators.js +118 -0
- package/dist/src/cli/action-binding.js +63 -0
- package/dist/src/cli/agent-commands.js +39 -0
- package/dist/src/cli/auth-commands.js +47 -0
- package/dist/src/cli/dam-commands.js +207 -0
- package/dist/src/cli/editor-commands.js +73 -0
- package/dist/src/cli/errors.js +90 -0
- package/dist/src/cli/model-commands.js +47 -0
- package/dist/src/cli/org-commands.js +52 -0
- package/dist/src/cli/presenter.js +80 -0
- package/dist/src/cli/prompt.js +30 -0
- package/dist/src/cli/tool-commands.js +65 -0
- package/dist/src/cli/update-command.js +12 -0
- package/dist/src/contracts/schema.js +4 -0
- package/dist/src/features/agent/creative-agent-adapter.js +31 -0
- package/dist/src/features/agent/creative-protocol.js +324 -0
- package/dist/src/features/agent/creative-stream.js +127 -0
- package/dist/src/features/agent/use-cases.js +113 -0
- package/dist/src/features/auth/access-policy.js +101 -0
- package/dist/src/features/auth/credential-store.js +62 -0
- package/dist/src/features/auth/sso-service.js +179 -0
- package/dist/src/features/auth/state.js +129 -0
- package/dist/src/features/auth/use-cases.js +106 -0
- package/dist/src/features/dam/asset-projection.js +270 -0
- package/dist/src/features/dam/dam-api-adapter.js +206 -0
- package/dist/src/features/dam/object-storage.js +191 -0
- package/dist/src/features/dam/registered-uploader.js +224 -0
- package/dist/src/features/dam/storage-upload.js +141 -0
- package/dist/src/features/dam/transient-uploader.js +17 -0
- package/dist/src/features/dam/use-cases.js +151 -0
- package/dist/src/features/editor/bridge-client.js +96 -0
- package/dist/src/features/editor/bridge-process.js +222 -0
- package/dist/src/features/editor/bridge-server.js +311 -0
- package/dist/src/features/editor/protocol.js +1 -0
- package/dist/src/features/editor/session-state.js +78 -0
- package/dist/src/features/editor/session.js +193 -0
- package/dist/src/features/editor/use-cases.js +61 -0
- package/dist/src/features/org/org-service.js +76 -0
- package/dist/src/features/org/use-cases.js +140 -0
- package/dist/src/features/skill/bundled-skills.js +55 -0
- package/dist/src/features/skill/installer.js +265 -0
- package/dist/src/features/tool/catalog.js +79 -0
- package/dist/src/features/tool/dynamic-schema.js +96 -0
- package/dist/src/features/tool/mns-catalog-adapter.js +262 -0
- package/dist/src/features/tool/tool-api-adapter.js +200 -0
- package/dist/src/features/tool/use-cases.js +126 -0
- package/dist/src/features/update/update-service.js +194 -0
- package/dist/src/platform/json-input.js +64 -0
- package/dist/src/platform/local-json-file.js +59 -0
- package/dist/src/platform/open-browser.js +8 -0
- package/dist/src/platform/redact.js +80 -0
- package/dist/src/platform/safe-upload-file.js +146 -0
- package/dist/src/platform/signature.js +19 -0
- package/dist/src/platform/signed-http-transport.js +109 -0
- package/dist/src/platform/url-safety.js +100 -0
- package/package.json +56 -0
- package/skills/gd-cli/SKILL.md +15 -0
- package/skills/gd-cli/references/auth-org.md +9 -0
- package/skills/gd-cli/references/creation.md +38 -0
- package/skills/gd-cli/references/dam.md +20 -0
- package/skills/gd-cli/references/editor.md +12 -0
- package/skills/gd-cli/references/errors.md +10 -0
- package/skills/gd-cli/references/update.md +11 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createEditorBridgeClient } from "./bridge-client.js";
|
|
2
|
+
import { startEditorBridgeProcess } from "./bridge-process.js";
|
|
3
|
+
const DEFAULT_CREATE_URL = new URL("https://www.gaoding.art/editor/canvas?mode=create&type=board");
|
|
4
|
+
const DEFAULT_WORK_URL = new URL("https://www.gaoding.art/editor/canvas");
|
|
5
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 60_000;
|
|
6
|
+
const CONNECTION_POLL_INTERVAL_MS = 100;
|
|
7
|
+
export class EditorTargetError extends Error {
|
|
8
|
+
code = "USAGE_ERROR";
|
|
9
|
+
constructor() {
|
|
10
|
+
super("Editor target 必须是作品 ID 或完整 HTTP(S) URL。");
|
|
11
|
+
this.name = "EditorTargetError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class EditorSessionNotFoundError extends Error {
|
|
15
|
+
code = "EDITOR_SESSION_NOT_FOUND";
|
|
16
|
+
constructor() {
|
|
17
|
+
super("当前没有 Editor 会话。");
|
|
18
|
+
this.name = "EditorSessionNotFoundError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class EditorNotReadyError extends Error {
|
|
22
|
+
code = "EDITOR_NOT_READY";
|
|
23
|
+
constructor() {
|
|
24
|
+
super("Editor 页面未在限定时间内就绪。");
|
|
25
|
+
this.name = "EditorNotReadyError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function resolveEditorTarget(value) {
|
|
29
|
+
return resolveTarget(value, DEFAULT_CREATE_URL, DEFAULT_WORK_URL).href;
|
|
30
|
+
}
|
|
31
|
+
export function createEditorSession(dependencies) {
|
|
32
|
+
const startProcess = dependencies.startProcess ?? startEditorBridgeProcess;
|
|
33
|
+
const createClient = dependencies.createClient ?? ((state) => createEditorBridgeClient({ state }));
|
|
34
|
+
const sleep = dependencies.sleep ?? abortableSleep;
|
|
35
|
+
const connectionTimeoutMs = Math.max(0, dependencies.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS);
|
|
36
|
+
return {
|
|
37
|
+
async connect(target, signal) {
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
const targetUrl = resolveTarget(target, dependencies.defaultCreateUrl, dependencies.workUrl);
|
|
40
|
+
await replaceCurrentSession(signal);
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
const state = await startProcess({
|
|
43
|
+
targetOrigin: targetUrl.origin,
|
|
44
|
+
...(dependencies.homeDirectory === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { homeDirectory: dependencies.homeDirectory }),
|
|
47
|
+
signal
|
|
48
|
+
});
|
|
49
|
+
const handoffUrl = buildHandoffUrl(targetUrl, state);
|
|
50
|
+
try {
|
|
51
|
+
signal.throwIfAborted();
|
|
52
|
+
await dependencies.browser.open(handoffUrl);
|
|
53
|
+
const client = createClient(state);
|
|
54
|
+
await waitForPage(client, signal);
|
|
55
|
+
await client.rpc("preflight", undefined, signal);
|
|
56
|
+
}
|
|
57
|
+
catch (cause) {
|
|
58
|
+
await cleanupFailedSession(state);
|
|
59
|
+
throw cause;
|
|
60
|
+
}
|
|
61
|
+
return { target: targetUrl.href };
|
|
62
|
+
},
|
|
63
|
+
async disconnect(signal) {
|
|
64
|
+
signal.throwIfAborted();
|
|
65
|
+
const state = await dependencies.store.read();
|
|
66
|
+
if (state === null)
|
|
67
|
+
return { disconnected: false };
|
|
68
|
+
const client = createClient(state);
|
|
69
|
+
try {
|
|
70
|
+
await client.health(signal);
|
|
71
|
+
}
|
|
72
|
+
catch (cause) {
|
|
73
|
+
signal.throwIfAborted();
|
|
74
|
+
await dependencies.store.removeIfOwned(state.token);
|
|
75
|
+
throw cause;
|
|
76
|
+
}
|
|
77
|
+
await client.disconnect(signal);
|
|
78
|
+
await dependencies.store.removeIfOwned(state.token);
|
|
79
|
+
return { disconnected: true };
|
|
80
|
+
},
|
|
81
|
+
async rpc(method, payload, signal) {
|
|
82
|
+
signal.throwIfAborted();
|
|
83
|
+
const state = await dependencies.store.read();
|
|
84
|
+
if (state === null)
|
|
85
|
+
throw new EditorSessionNotFoundError();
|
|
86
|
+
return createClient(state).rpc(method, payload, signal);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
async function replaceCurrentSession(signal) {
|
|
90
|
+
const current = await dependencies.store.read();
|
|
91
|
+
if (current === null)
|
|
92
|
+
return;
|
|
93
|
+
const client = createClient(current);
|
|
94
|
+
try {
|
|
95
|
+
await client.health(signal);
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
signal.throwIfAborted();
|
|
99
|
+
await dependencies.store.removeIfOwned(current.token);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await client.disconnect(signal);
|
|
103
|
+
await dependencies.store.removeIfOwned(current.token);
|
|
104
|
+
}
|
|
105
|
+
async function waitForPage(client, signal) {
|
|
106
|
+
const deadline = Date.now() + connectionTimeoutMs;
|
|
107
|
+
while (true) {
|
|
108
|
+
signal.throwIfAborted();
|
|
109
|
+
if ((await client.health(signal)).pageConnected)
|
|
110
|
+
return;
|
|
111
|
+
const remaining = deadline - Date.now();
|
|
112
|
+
if (remaining <= 0)
|
|
113
|
+
throw new EditorNotReadyError();
|
|
114
|
+
await sleep(Math.min(CONNECTION_POLL_INTERVAL_MS, remaining), signal);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function cleanupFailedSession(state) {
|
|
118
|
+
try {
|
|
119
|
+
const current = await dependencies.store.read();
|
|
120
|
+
if (current !== null && current.token !== state.token)
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Continue best-effort cleanup without replacing the original failure.
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await createClient(state).disconnect(new AbortController().signal);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// The Bridge may already have stopped.
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const current = await dependencies.store.read();
|
|
134
|
+
if (current?.token === state.token) {
|
|
135
|
+
await dependencies.store.removeIfOwned(state.token);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Preserve the browser, wait, or preflight failure.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function resolveTarget(value, createUrl, workUrl) {
|
|
144
|
+
if (value === undefined)
|
|
145
|
+
return new URL(createUrl.href);
|
|
146
|
+
if (/^\d+$/.test(value)) {
|
|
147
|
+
const url = new URL(workUrl.href);
|
|
148
|
+
url.search = new URLSearchParams({
|
|
149
|
+
type: "board",
|
|
150
|
+
mode: "user",
|
|
151
|
+
id: value
|
|
152
|
+
}).toString();
|
|
153
|
+
url.hash = "";
|
|
154
|
+
return url;
|
|
155
|
+
}
|
|
156
|
+
let url;
|
|
157
|
+
try {
|
|
158
|
+
url = new URL(value);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
throw new EditorTargetError();
|
|
162
|
+
}
|
|
163
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
164
|
+
url.username !== "" ||
|
|
165
|
+
url.password !== "") {
|
|
166
|
+
throw new EditorTargetError();
|
|
167
|
+
}
|
|
168
|
+
return url;
|
|
169
|
+
}
|
|
170
|
+
function buildHandoffUrl(target, state) {
|
|
171
|
+
const url = new URL(target.href);
|
|
172
|
+
url.hash = new URLSearchParams({
|
|
173
|
+
"focused-editor-port": String(state.port),
|
|
174
|
+
"focused-editor-token": state.token
|
|
175
|
+
}).toString();
|
|
176
|
+
return url;
|
|
177
|
+
}
|
|
178
|
+
function abortableSleep(milliseconds, signal) {
|
|
179
|
+
signal.throwIfAborted();
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const timer = setTimeout(done, milliseconds);
|
|
182
|
+
const onAbort = () => {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
signal.removeEventListener("abort", onAbort);
|
|
185
|
+
reject(signal.reason);
|
|
186
|
+
};
|
|
187
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
188
|
+
function done() {
|
|
189
|
+
signal.removeEventListener("abort", onAbort);
|
|
190
|
+
resolve();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { OutputContractError } from "../../bootstrap/validators.js";
|
|
5
|
+
const PNG_DATA_URL_PREFIX = "data:image/png;base64,";
|
|
6
|
+
const STRICT_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
7
|
+
export function createEditorUseCases(dependencies) {
|
|
8
|
+
const validate = dependencies.validate;
|
|
9
|
+
const temporaryDirectory = resolve(dependencies.temporaryDirectory ?? tmpdir());
|
|
10
|
+
return {
|
|
11
|
+
async connect({ input, signal }) {
|
|
12
|
+
const result = await dependencies.session.connect(input.target, signal);
|
|
13
|
+
validate.connect(result);
|
|
14
|
+
return result;
|
|
15
|
+
},
|
|
16
|
+
async disconnect({ signal }) {
|
|
17
|
+
const result = await dependencies.session.disconnect(signal);
|
|
18
|
+
validate.disconnect(result);
|
|
19
|
+
return result;
|
|
20
|
+
},
|
|
21
|
+
async snapshot({ signal }) {
|
|
22
|
+
const result = await dependencies.session.rpc("snapshot", undefined, signal);
|
|
23
|
+
validate.snapshot(result);
|
|
24
|
+
return result;
|
|
25
|
+
},
|
|
26
|
+
async apply({ input, signal }) {
|
|
27
|
+
await dependencies.session.rpc("apply", input, signal);
|
|
28
|
+
const result = { appliedCount: input.length };
|
|
29
|
+
validate.apply(result);
|
|
30
|
+
return result;
|
|
31
|
+
},
|
|
32
|
+
async screenshot({ signal }) {
|
|
33
|
+
const data = await dependencies.session.rpc("screenshot", undefined, signal);
|
|
34
|
+
const bytes = decodePngDataUrl(data);
|
|
35
|
+
signal.throwIfAborted();
|
|
36
|
+
const directory = await mkdtemp(join(temporaryDirectory, "gd-cli-editor-"));
|
|
37
|
+
const result = { path: join(directory, "screenshot.png") };
|
|
38
|
+
await writeFile(result.path, bytes);
|
|
39
|
+
validate.screenshot(result);
|
|
40
|
+
return result;
|
|
41
|
+
},
|
|
42
|
+
async save({ signal }) {
|
|
43
|
+
const result = await dependencies.session.rpc("save", undefined, signal);
|
|
44
|
+
validate.save(result);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function decodePngDataUrl(value) {
|
|
50
|
+
if (typeof value !== "string" || !value.startsWith(PNG_DATA_URL_PREFIX)) {
|
|
51
|
+
throw new OutputContractError();
|
|
52
|
+
}
|
|
53
|
+
const encoded = value.slice(PNG_DATA_URL_PREFIX.length);
|
|
54
|
+
if (encoded.length === 0 || !STRICT_BASE64.test(encoded)) {
|
|
55
|
+
throw new OutputContractError();
|
|
56
|
+
}
|
|
57
|
+
const bytes = Buffer.from(encoded, "base64");
|
|
58
|
+
if (bytes.toString("base64") !== encoded)
|
|
59
|
+
throw new OutputContractError();
|
|
60
|
+
return bytes;
|
|
61
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export class OrganizationResponseError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("组织列表响应无效。");
|
|
4
|
+
this.name = "OrganizationResponseError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function createOrgService(dependencies) {
|
|
8
|
+
return {
|
|
9
|
+
async list(credential, signal) {
|
|
10
|
+
const raw = await dependencies.transport.getJson({
|
|
11
|
+
path: "/structure/user/orgs",
|
|
12
|
+
credential,
|
|
13
|
+
signal
|
|
14
|
+
});
|
|
15
|
+
if (!Array.isArray(raw))
|
|
16
|
+
throw new OrganizationResponseError();
|
|
17
|
+
const records = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const item of raw) {
|
|
20
|
+
const record = normalizeOrganization(item);
|
|
21
|
+
if (!record || seen.has(record.id))
|
|
22
|
+
continue;
|
|
23
|
+
seen.add(record.id);
|
|
24
|
+
records.push(record);
|
|
25
|
+
}
|
|
26
|
+
return records;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function normalizeOrganization(value) {
|
|
31
|
+
if (!isRecord(value))
|
|
32
|
+
return null;
|
|
33
|
+
const company = isRecord(value.company) ? value.company : undefined;
|
|
34
|
+
const product = isRecord(value.product) ? value.product : undefined;
|
|
35
|
+
const id = identifier(value.org_id) ?? identifier(company?.id);
|
|
36
|
+
if (!id)
|
|
37
|
+
return null;
|
|
38
|
+
const realName = optionalString(company?.name) ?? null;
|
|
39
|
+
const expiry = finiteNumber(value.expire_date) ?? finiteNumber(product?.expire_date);
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
publicName: realName ?? `组织 ${id}`,
|
|
43
|
+
realName,
|
|
44
|
+
...optionalField("type", optionalString(company?.org_class)),
|
|
45
|
+
...optionalField("status", optionalString(company?.status)),
|
|
46
|
+
...optionalField("productName", optionalString(product?.product_name)),
|
|
47
|
+
...optionalField("productType", optionalString(product?.product_type)),
|
|
48
|
+
...optionalField("expiresAt", epochMilliseconds(expiry))
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function optionalField(key, value) {
|
|
52
|
+
return value === undefined ? {} : { [key]: value };
|
|
53
|
+
}
|
|
54
|
+
function epochMilliseconds(value) {
|
|
55
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
56
|
+
return undefined;
|
|
57
|
+
const date = new Date(value);
|
|
58
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
59
|
+
}
|
|
60
|
+
function finiteNumber(value) {
|
|
61
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
62
|
+
}
|
|
63
|
+
function identifier(value) {
|
|
64
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
65
|
+
return String(value);
|
|
66
|
+
return optionalString(value);
|
|
67
|
+
}
|
|
68
|
+
function optionalString(value) {
|
|
69
|
+
if (typeof value !== "string")
|
|
70
|
+
return undefined;
|
|
71
|
+
const text = value.trim();
|
|
72
|
+
return text || undefined;
|
|
73
|
+
}
|
|
74
|
+
function isRecord(value) {
|
|
75
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
76
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { classifyBindOutcome, SsoRequestError } from "../auth/sso-service.js";
|
|
2
|
+
import { RemoteRequestError } from "../../platform/signed-http-transport.js";
|
|
3
|
+
import { OrganizationResponseError } from "./org-service.js";
|
|
4
|
+
export class OrgUsageError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("请通过 --org 指定组织,或在交互终端中选择组织。");
|
|
7
|
+
this.name = "OrgUsageError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class OrganizationInvalidError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("所选组织不可用,请执行 gd-cli org list 确认可用组织。");
|
|
13
|
+
this.name = "OrganizationInvalidError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class OrganizationBindFailedError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("选择组织失败,请稍后重试。");
|
|
19
|
+
this.name = "OrganizationBindFailedError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function createOrgUseCases(dependencies) {
|
|
23
|
+
async function bind(credential, record, signal) {
|
|
24
|
+
const response = await dependencies.sso.bindOrganization({
|
|
25
|
+
credential,
|
|
26
|
+
organizationId: record.id
|
|
27
|
+
}, signal);
|
|
28
|
+
const outcome = classifyBindOutcome(response);
|
|
29
|
+
if (outcome.status === "invalid")
|
|
30
|
+
throw new OrganizationInvalidError();
|
|
31
|
+
if (outcome.status === "failed")
|
|
32
|
+
throw new OrganizationBindFailedError();
|
|
33
|
+
return {
|
|
34
|
+
id: record.id,
|
|
35
|
+
name: record.publicName,
|
|
36
|
+
...(outcome.bindExpiresAt ? { bindExpiresAt: outcome.bindExpiresAt } : {})
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
async list(input) {
|
|
41
|
+
const records = await dependencies.org.list(input.state.credential, input.signal);
|
|
42
|
+
return {
|
|
43
|
+
orgs: records.map((record) => ({
|
|
44
|
+
id: record.id,
|
|
45
|
+
name: record.publicName,
|
|
46
|
+
...(record.type ? { type: record.type } : {}),
|
|
47
|
+
...(record.status ? { status: record.status } : {}),
|
|
48
|
+
...(record.productName ? { product_name: record.productName } : {}),
|
|
49
|
+
...(record.productType ? { product_type: record.productType } : {}),
|
|
50
|
+
...(record.expiresAt ? { expires_at: record.expiresAt } : {})
|
|
51
|
+
})),
|
|
52
|
+
current_org_id: input.state.organization?.id ?? null
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
async current() {
|
|
56
|
+
const state = await dependencies.store.read();
|
|
57
|
+
return {
|
|
58
|
+
organization: state?.organization
|
|
59
|
+
? { id: state.organization.id, name: state.organization.name }
|
|
60
|
+
: null,
|
|
61
|
+
next_steps: state?.organization
|
|
62
|
+
? []
|
|
63
|
+
: ["gd-cli org list", "gd-cli org switch"]
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
async switchOrganization(input) {
|
|
67
|
+
input.signal.throwIfAborted();
|
|
68
|
+
const records = await dependencies.org.list(input.state.credential, input.signal);
|
|
69
|
+
const selectedId = input.organizationId === undefined
|
|
70
|
+
? await selectOrganization(records, input.select)
|
|
71
|
+
: requiredSelection(input.organizationId);
|
|
72
|
+
const record = records.find((candidate) => candidate.id === selectedId);
|
|
73
|
+
if (!record)
|
|
74
|
+
throw new OrganizationInvalidError();
|
|
75
|
+
const organization = await bind(input.state.credential, record, input.signal);
|
|
76
|
+
await dependencies.store.write({ ...input.state, organization });
|
|
77
|
+
return record;
|
|
78
|
+
},
|
|
79
|
+
async bindAuthorizedOrganization(input) {
|
|
80
|
+
input.signal.throwIfAborted();
|
|
81
|
+
const organizationId = input.organizationId.trim();
|
|
82
|
+
if (!organizationId)
|
|
83
|
+
return null;
|
|
84
|
+
try {
|
|
85
|
+
const records = await dependencies.org.list(input.credential, input.signal);
|
|
86
|
+
const record = records.find((candidate) => candidate.id === organizationId);
|
|
87
|
+
if (!record?.realName)
|
|
88
|
+
return null;
|
|
89
|
+
const bound = await bind(input.credential, record, input.signal);
|
|
90
|
+
return {
|
|
91
|
+
id: bound.id,
|
|
92
|
+
name: record.realName,
|
|
93
|
+
...(bound.bindExpiresAt ? { bindExpiresAt: bound.bindExpiresAt } : {})
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (input.signal.aborted)
|
|
98
|
+
throw input.signal.reason;
|
|
99
|
+
if (isExpectedAuthorizationFailure(error))
|
|
100
|
+
return null;
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
async refreshCurrentOrganization(input) {
|
|
105
|
+
input.signal.throwIfAborted();
|
|
106
|
+
const response = await dependencies.sso.bindOrganization({
|
|
107
|
+
credential: input.state.credential,
|
|
108
|
+
organizationId: input.state.organization.id
|
|
109
|
+
}, input.signal);
|
|
110
|
+
const outcome = classifyBindOutcome(response);
|
|
111
|
+
if (outcome.status === "invalid")
|
|
112
|
+
throw new OrganizationInvalidError();
|
|
113
|
+
if (outcome.status === "failed")
|
|
114
|
+
throw new OrganizationBindFailedError();
|
|
115
|
+
return {
|
|
116
|
+
id: input.state.organization.id,
|
|
117
|
+
name: input.state.organization.name,
|
|
118
|
+
...(outcome.bindExpiresAt ? { bindExpiresAt: outcome.bindExpiresAt } : {})
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function selectOrganization(records, select) {
|
|
124
|
+
if (!select)
|
|
125
|
+
throw new OrgUsageError();
|
|
126
|
+
return requiredSelection(await select(records));
|
|
127
|
+
}
|
|
128
|
+
function requiredSelection(value) {
|
|
129
|
+
const selected = value.trim();
|
|
130
|
+
if (!selected)
|
|
131
|
+
throw new OrgUsageError();
|
|
132
|
+
return selected;
|
|
133
|
+
}
|
|
134
|
+
function isExpectedAuthorizationFailure(error) {
|
|
135
|
+
return error instanceof RemoteRequestError
|
|
136
|
+
|| error instanceof SsoRequestError
|
|
137
|
+
|| error instanceof OrganizationResponseError
|
|
138
|
+
|| error instanceof OrganizationInvalidError
|
|
139
|
+
|| error instanceof OrganizationBindFailedError;
|
|
140
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
3
|
+
import { SkillSyncError } from "./installer.js";
|
|
4
|
+
export function loadBundledSkills(startFile) {
|
|
5
|
+
let directory = directoryFrom(startFile);
|
|
6
|
+
const root = parse(directory).root;
|
|
7
|
+
while (directory !== root) {
|
|
8
|
+
const packageFile = join(directory, "package.json");
|
|
9
|
+
if (isFile(packageFile)) {
|
|
10
|
+
let metadata;
|
|
11
|
+
try {
|
|
12
|
+
metadata = JSON.parse(readFileSync(packageFile, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
throw new SkillSyncError(`无法读取 gaoding-cli package metadata: ${message(error)}`, []);
|
|
16
|
+
}
|
|
17
|
+
if (metadata.name === "gaoding-cli") {
|
|
18
|
+
if (typeof metadata.version !== "string" || !metadata.version.trim()) {
|
|
19
|
+
throw new SkillSyncError("gaoding-cli package version 无效。", []);
|
|
20
|
+
}
|
|
21
|
+
const sourceDirectory = join(directory, "skills", "gd-cli");
|
|
22
|
+
if (!isFile(join(sourceDirectory, "SKILL.md"))) {
|
|
23
|
+
throw new SkillSyncError(`Agent Skill source 不存在: ${sourceDirectory}`, []);
|
|
24
|
+
}
|
|
25
|
+
return [{
|
|
26
|
+
skill: "gd-cli",
|
|
27
|
+
version: metadata.version,
|
|
28
|
+
sourceDirectory
|
|
29
|
+
}];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
directory = dirname(directory);
|
|
33
|
+
}
|
|
34
|
+
throw new SkillSyncError("无法定位 gaoding-cli package root。", []);
|
|
35
|
+
}
|
|
36
|
+
function directoryFrom(path) {
|
|
37
|
+
const absolute = resolve(path);
|
|
38
|
+
try {
|
|
39
|
+
return lstatSync(absolute).isDirectory() ? absolute : dirname(absolute);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return dirname(absolute);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isFile(path) {
|
|
46
|
+
try {
|
|
47
|
+
return lstatSync(path).isFile();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function message(error) {
|
|
54
|
+
return error instanceof Error ? error.message : String(error);
|
|
55
|
+
}
|