@doubao-dev/plugin-cli 0.0.0-stage → 0.0.40-canary-dea0ddc73-20261026073950
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.
Potentially problematic release.
This version of @doubao-dev/plugin-cli might be problematic. Click here for more details.
- package/README.md +16 -2
- package/bin/run.js +16 -0
- package/dist/188.js +1691 -0
- package/dist/categories.js +8 -0
- package/dist/command.js +184 -0
- package/dist/command~1.js +45 -0
- package/dist/command~2.js +14 -0
- package/dist/command~3.js +91 -0
- package/dist/command~4.js +191 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +5 -0
- package/dist/main.d.ts +163 -0
- package/dist/main.js +1 -0
- package/dist/plugin.js +97 -0
- package/dist/session.js +248 -0
- package/dist/skills/doubao-plugin-cli/SKILL.md +72 -0
- package/dist/skills/doubao-plugin-cli/references/cli-connectors.md +25 -0
- package/dist/skills/doubao-plugin-cli/references/mcp-apps.md +70 -0
- package/dist/skills/doubao-plugin-cli/references/mcp-auth.md +23 -0
- package/dist/skills/doubao-plugin-cli/references/project-config.md +105 -0
- package/dist/skills/doubao-plugin-cli/references/upload.md +53 -0
- package/dist/templates/standard/AGENTS.md +5 -0
- package/dist/templates/standard/README.md.template +9 -0
- package/dist/templates/standard/mcp.json +4 -0
- package/dist/templates/standard/plugin.json.template +6 -0
- package/dist/uninstall.js +12 -0
- package/package.json +53 -3
package/dist/188.js
ADDED
|
@@ -0,0 +1,1691 @@
|
|
|
1
|
+
import { Command, CommanderError, Option } from "commander";
|
|
2
|
+
import strict from "node:assert/strict";
|
|
3
|
+
import node_crypto, { randomUUID } from "node:crypto";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import node_os, { homedir } from "node:os";
|
|
6
|
+
import node_path, { dirname, join, resolve as external_node_path_resolve } from "node:path";
|
|
7
|
+
import { execFile, spawn } from "node:child_process";
|
|
8
|
+
import node_fs, { existsSync, promises } from "node:fs";
|
|
9
|
+
import { promisify, stripVTControlCharacters } from "node:util";
|
|
10
|
+
import { setTimeout as promises_setTimeout } from "node:timers/promises";
|
|
11
|
+
import { ofetch } from "ofetch";
|
|
12
|
+
import { once } from "node:events";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
import ora, { oraPromise } from "ora";
|
|
16
|
+
import { log } from "@clack/prompts";
|
|
17
|
+
import { readJsonSync } from "fs-extra/esm";
|
|
18
|
+
import * as __rspack_external_node_fs_1b05aee1 from "node:fs";
|
|
19
|
+
function errorMessage(error) {
|
|
20
|
+
return error instanceof Error ? error.message : String(error);
|
|
21
|
+
}
|
|
22
|
+
async function applyPlugins(program, plugins) {
|
|
23
|
+
const names = new Set();
|
|
24
|
+
for (const { name } of plugins){
|
|
25
|
+
strict(name.trim() && !names.has(name), `插件名称为空或重复:${name}`);
|
|
26
|
+
names.add(name);
|
|
27
|
+
}
|
|
28
|
+
const hooks = {
|
|
29
|
+
addCommand: [],
|
|
30
|
+
dev: [],
|
|
31
|
+
modifyPackagePlan: [],
|
|
32
|
+
afterUpload: []
|
|
33
|
+
};
|
|
34
|
+
for (const plugin of plugins)try {
|
|
35
|
+
const pluginName = plugin.name;
|
|
36
|
+
await plugin.setup({
|
|
37
|
+
onAddCommand: (callback)=>{
|
|
38
|
+
hooks.addCommand.push({
|
|
39
|
+
pluginName,
|
|
40
|
+
callback
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
onDev: (callback)=>{
|
|
44
|
+
hooks.dev.push({
|
|
45
|
+
pluginName,
|
|
46
|
+
callback
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
onModifyPackagePlan: (callback)=>{
|
|
50
|
+
hooks.modifyPackagePlan.push({
|
|
51
|
+
pluginName,
|
|
52
|
+
callback
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
onAfterUpload: (callback)=>{
|
|
56
|
+
hooks.afterUpload.push({
|
|
57
|
+
pluginName,
|
|
58
|
+
callback
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
throw new Error(`[${plugin.name}] 注册失败:${errorMessage(cause)}`, {
|
|
64
|
+
cause
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const hook of hooks.addCommand)await callHook(hook, {
|
|
68
|
+
program,
|
|
69
|
+
hooks
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function callHook({ pluginName, callback }, ...args) {
|
|
73
|
+
try {
|
|
74
|
+
return await callback(...args);
|
|
75
|
+
} catch (cause) {
|
|
76
|
+
throw new Error(`[${pluginName}] ${errorMessage(cause)}`, {
|
|
77
|
+
cause
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
var package_namespaceObject = /*#__PURE__*/ JSON.parse('{"UU":"@doubao-dev/plugin-cli","rE":"0.0.40-canary-dea0ddc73-20261026073950","h_":"Doubao Plugins CLI"}');
|
|
82
|
+
const runtime = {
|
|
83
|
+
name: package_namespaceObject.UU,
|
|
84
|
+
version: package_namespaceObject.rE,
|
|
85
|
+
description: package_namespaceObject.h_,
|
|
86
|
+
registry: 'https://registry.npmjs.org',
|
|
87
|
+
appDev: {
|
|
88
|
+
name: '@doubao-dev/plugin-app-dev',
|
|
89
|
+
version: package_namespaceObject.rE,
|
|
90
|
+
entry: '.'
|
|
91
|
+
},
|
|
92
|
+
kit: {
|
|
93
|
+
name: '@doubao-dev/kit',
|
|
94
|
+
version: process.env.DBX_KIT_VERSION?.trim() || "0.8.0-rc.2",
|
|
95
|
+
entry: './sdk'
|
|
96
|
+
},
|
|
97
|
+
auth: {
|
|
98
|
+
baseUrl: 'https://open.doubao.com',
|
|
99
|
+
verificationUrl: 'https://open.doubao.com/apps-plugin/oauth/device'
|
|
100
|
+
},
|
|
101
|
+
apiUrl: 'https://open.doubao.com',
|
|
102
|
+
deviceApiUrl: 'https://developer.doubao.com',
|
|
103
|
+
consoleUrl: 'https://open.doubao.com/console/plugins/create'
|
|
104
|
+
};
|
|
105
|
+
const DEFAULT_BASE_URL = 'https://developer.doubao.com';
|
|
106
|
+
const DEFAULT_CLIENT_NAME = 'dbx-cli';
|
|
107
|
+
const DEFAULT_TOKEN_PATH = node_path.join(node_os.homedir(), '.dbx', 'token');
|
|
108
|
+
const LEGACY_TOKEN_PATH = node_path.join(node_os.homedir(), '.doubao', 'token');
|
|
109
|
+
const configured = {};
|
|
110
|
+
function config_resolveOptions(options = {}) {
|
|
111
|
+
const merged = {
|
|
112
|
+
...configured,
|
|
113
|
+
...options
|
|
114
|
+
};
|
|
115
|
+
const baseUrl = normalizeHttpsBaseUrl(merged.baseUrl ?? DEFAULT_BASE_URL, 'baseUrl');
|
|
116
|
+
const tokenPath = merged.tokenPath ?? DEFAULT_TOKEN_PATH;
|
|
117
|
+
return {
|
|
118
|
+
...merged,
|
|
119
|
+
baseUrl,
|
|
120
|
+
verificationUrl: normalizeHttpsBaseUrl(merged.verificationUrl ?? new URL('/oauth/device', baseUrl).href, 'verificationUrl'),
|
|
121
|
+
allowedOrigins: normalizeAllowedOrigins(merged.allowedOrigins, baseUrl),
|
|
122
|
+
tokenPath,
|
|
123
|
+
legacyTokenPath: merged.tokenPath ? void 0 : LEGACY_TOKEN_PATH,
|
|
124
|
+
requestHeaders: new Headers(merged.requestHeaders)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function trimTrailingSlash(value) {
|
|
128
|
+
return value.replace(/\/+$/, '');
|
|
129
|
+
}
|
|
130
|
+
function normalizeHttpsBaseUrl(value, label) {
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = new URL(trimTrailingSlash(value));
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(`${label} 必须是完整的 HTTPS URL`);
|
|
136
|
+
}
|
|
137
|
+
if ('https:' !== parsed.protocol) throw new Error(`${label} 必须使用 HTTPS`);
|
|
138
|
+
return trimTrailingSlash(parsed.toString());
|
|
139
|
+
}
|
|
140
|
+
function normalizeAllowedOrigins(values, baseUrl) {
|
|
141
|
+
const origins = new Set([
|
|
142
|
+
new URL(baseUrl).origin
|
|
143
|
+
]);
|
|
144
|
+
for (const value of values ?? []){
|
|
145
|
+
let parsed;
|
|
146
|
+
try {
|
|
147
|
+
parsed = new URL(value);
|
|
148
|
+
} catch {
|
|
149
|
+
throw new Error(`allowedOrigins 中的来源必须是完整的 HTTPS 地址:${value}`);
|
|
150
|
+
}
|
|
151
|
+
if ('https:' !== parsed.protocol) throw new Error(`allowedOrigins 中的来源必须使用 HTTPS:${value}`);
|
|
152
|
+
origins.add(parsed.origin);
|
|
153
|
+
}
|
|
154
|
+
return [
|
|
155
|
+
...origins
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
const execFileAsync = promisify(execFile);
|
|
159
|
+
const SERVICE_NAME = 'byted-doubao-open-dbx-cli';
|
|
160
|
+
const LOCAL_STORE_DIR_NAME = 'dbx-cli';
|
|
161
|
+
const MASTER_KEY_FILE = 'master.key';
|
|
162
|
+
const CREDENTIALS_DIR = 'credentials';
|
|
163
|
+
const AUTHORITIES_DIR = 'authorities';
|
|
164
|
+
const LOCAL_STORE_VERSION = 1;
|
|
165
|
+
const LOCAL_STORE_ALGORITHM = 'aes-256-gcm';
|
|
166
|
+
class CredentialStoreError extends Error {
|
|
167
|
+
constructor(message){
|
|
168
|
+
super(message);
|
|
169
|
+
this.name = 'CredentialStoreError';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
class MissingMasterKeyError extends CredentialStoreError {
|
|
173
|
+
constructor(){
|
|
174
|
+
super('无法读取本地登录凭证:用于解密凭证的密钥文件 master.key 已丢失。请运行 dbx auth login 重新登录。');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function defaultCredentialStore() {
|
|
178
|
+
const encryptedFileStore = new EncryptedFileCredentialStore();
|
|
179
|
+
if ('darwin' === process.platform) return new CredentialStoreWithFallback(new MacOSKeychainStore(), encryptedFileStore);
|
|
180
|
+
if ('linux' === process.platform) return new CredentialStoreWithFallback(new SecretToolStore(), encryptedFileStore);
|
|
181
|
+
return encryptedFileStore;
|
|
182
|
+
}
|
|
183
|
+
class CredentialStoreWithFallback {
|
|
184
|
+
constructor(primary, fallback){
|
|
185
|
+
this.primary = primary;
|
|
186
|
+
this.fallback = fallback;
|
|
187
|
+
}
|
|
188
|
+
async get(account) {
|
|
189
|
+
if (await this.isFallbackAuthoritative(account)) return this.fallback.get(account);
|
|
190
|
+
let primaryError;
|
|
191
|
+
try {
|
|
192
|
+
const value = await this.primary.get(account);
|
|
193
|
+
if (value) return value;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
primaryError = error;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const value = await this.fallback.get(account);
|
|
199
|
+
if (value) return value;
|
|
200
|
+
} catch (fallbackError) {
|
|
201
|
+
if (primaryError) throw new CredentialStoreError(`从系统凭证存储读取登录凭证失败(${credential_store_errorMessage(primaryError)});从备用加密文件读取也失败:${credential_store_errorMessage(fallbackError)}`);
|
|
202
|
+
throw fallbackError;
|
|
203
|
+
}
|
|
204
|
+
if (primaryError) throw primaryError instanceof Error ? primaryError : new CredentialStoreError(`从系统凭证存储读取登录凭证失败:${String(primaryError)}`);
|
|
205
|
+
}
|
|
206
|
+
async set(account, value) {
|
|
207
|
+
let primaryError;
|
|
208
|
+
try {
|
|
209
|
+
await this.primary.set(account, value);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
primaryError = error;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
await this.fallback.set(account, value);
|
|
215
|
+
} catch (fallbackError) {
|
|
216
|
+
if (primaryError) throw new CredentialStoreError(`向系统凭证存储写入登录凭证失败(${credential_store_errorMessage(primaryError)}),写入备用加密文件也失败(${credential_store_errorMessage(fallbackError)})`);
|
|
217
|
+
await this.removeStaleFallbackAfterPrimaryWrite(account, fallbackError);
|
|
218
|
+
throw fallbackError;
|
|
219
|
+
}
|
|
220
|
+
if (primaryError) await this.markFallbackAuthoritative(account);
|
|
221
|
+
else await this.clearFallbackAuthority(account);
|
|
222
|
+
}
|
|
223
|
+
async delete(account) {
|
|
224
|
+
let firstError;
|
|
225
|
+
try {
|
|
226
|
+
await this.primary.delete(account);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
firstError = error;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
await this.fallback.delete(account);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (firstError) throw new CredentialStoreError(`从系统凭证存储删除登录凭证失败(${credential_store_errorMessage(firstError)}),从备用加密文件删除也失败(${credential_store_errorMessage(error)})`);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
if (firstError) return void await this.markFallbackAuthoritativeOrThrow(account, firstError);
|
|
237
|
+
await this.clearFallbackAuthority(account);
|
|
238
|
+
}
|
|
239
|
+
async isFallbackAuthoritative(account) {
|
|
240
|
+
return supportsCredentialAuthority(this.fallback) ? this.fallback.isFallbackAuthoritative(account) : false;
|
|
241
|
+
}
|
|
242
|
+
async markFallbackAuthoritative(account) {
|
|
243
|
+
if (supportsCredentialAuthority(this.fallback)) await this.fallback.markFallbackAuthoritative(account);
|
|
244
|
+
}
|
|
245
|
+
async markFallbackAuthoritativeOrThrow(account, primaryError) {
|
|
246
|
+
if (!supportsCredentialAuthority(this.fallback)) throw primaryError instanceof Error ? primaryError : new CredentialStoreError(`系统凭证删除失败,且备用存储无法记录删除状态:${String(primaryError)}`);
|
|
247
|
+
try {
|
|
248
|
+
await this.fallback.markFallbackAuthoritative(account);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new CredentialStoreError(`系统凭证删除失败(${credential_store_errorMessage(primaryError)}),且无法记录备用凭证删除状态:${credential_store_errorMessage(error)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async clearFallbackAuthority(account) {
|
|
254
|
+
if (supportsCredentialAuthority(this.fallback)) await this.fallback.clearFallbackAuthority(account);
|
|
255
|
+
}
|
|
256
|
+
async removeStaleFallbackAfterPrimaryWrite(account, fallbackError) {
|
|
257
|
+
const cleanupErrors = [];
|
|
258
|
+
try {
|
|
259
|
+
await this.fallback.delete(account);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
cleanupErrors.push(error);
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
await this.clearFallbackAuthority(account);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
cleanupErrors.push(error);
|
|
267
|
+
}
|
|
268
|
+
if (cleanupErrors.length > 0) throw new CredentialStoreError(`系统凭证已写入,但备用加密文件写入失败(${credential_store_errorMessage(fallbackError)}),且清理旧备用凭证失败:${cleanupErrors.map(credential_store_errorMessage).join(';')}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
class EncryptedFileCredentialStore {
|
|
272
|
+
constructor(options = {}){
|
|
273
|
+
this.baseDir = options.baseDir ?? defaultEncryptedCredentialDir();
|
|
274
|
+
}
|
|
275
|
+
async get(account) {
|
|
276
|
+
const filePath = this.credentialFilePath(account);
|
|
277
|
+
let raw;
|
|
278
|
+
try {
|
|
279
|
+
raw = await promises.readFile(filePath, 'utf8');
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (isNodeError(error) && 'ENOENT' === error.code) return;
|
|
282
|
+
throw new CredentialStoreError(`读取加密凭证文件失败:${credential_store_errorMessage(error)}`);
|
|
283
|
+
}
|
|
284
|
+
let record;
|
|
285
|
+
try {
|
|
286
|
+
record = JSON.parse(raw);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
throw new CredentialStoreError(`加密凭证文件不是有效的 JSON:${credential_store_errorMessage(error)}`);
|
|
289
|
+
}
|
|
290
|
+
if (!isEncryptedCredentialRecord(record)) throw new CredentialStoreError('无法读取本地登录凭证:当前 CLI 不支持凭证文件中的格式或字段。');
|
|
291
|
+
const key = await this.readMasterKey();
|
|
292
|
+
try {
|
|
293
|
+
const decipher = node_crypto.createDecipheriv(LOCAL_STORE_ALGORITHM, key, Buffer.from(record.iv, 'base64url'));
|
|
294
|
+
decipher.setAAD(Buffer.from(associatedData(account), 'utf8'));
|
|
295
|
+
decipher.setAuthTag(Buffer.from(record.authTag, 'base64url'));
|
|
296
|
+
const plaintext = Buffer.concat([
|
|
297
|
+
decipher.update(Buffer.from(record.ciphertext, 'base64url')),
|
|
298
|
+
decipher.final()
|
|
299
|
+
]);
|
|
300
|
+
return plaintext.toString('utf8');
|
|
301
|
+
} catch (error) {
|
|
302
|
+
throw new CredentialStoreError(`解密凭证文件失败:${credential_store_errorMessage(error)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async set(account, value) {
|
|
306
|
+
const key = await this.readOrCreateMasterKey();
|
|
307
|
+
const iv = node_crypto.randomBytes(12);
|
|
308
|
+
const cipher = node_crypto.createCipheriv(LOCAL_STORE_ALGORITHM, key, iv);
|
|
309
|
+
cipher.setAAD(Buffer.from(associatedData(account), 'utf8'));
|
|
310
|
+
const ciphertext = Buffer.concat([
|
|
311
|
+
cipher.update(value, 'utf8'),
|
|
312
|
+
cipher.final()
|
|
313
|
+
]);
|
|
314
|
+
const record = {
|
|
315
|
+
version: LOCAL_STORE_VERSION,
|
|
316
|
+
algorithm: LOCAL_STORE_ALGORITHM,
|
|
317
|
+
iv: iv.toString('base64url'),
|
|
318
|
+
authTag: cipher.getAuthTag().toString('base64url'),
|
|
319
|
+
ciphertext: ciphertext.toString('base64url')
|
|
320
|
+
};
|
|
321
|
+
await writePrivateFileAtomic(this.credentialFilePath(account), `${JSON.stringify(record, null, 2)}\n`);
|
|
322
|
+
}
|
|
323
|
+
async delete(account) {
|
|
324
|
+
try {
|
|
325
|
+
await promises.unlink(this.credentialFilePath(account));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (!isNodeError(error) || 'ENOENT' !== error.code) throw new CredentialStoreError(`删除加密凭证文件失败:${credential_store_errorMessage(error)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async isFallbackAuthoritative(account) {
|
|
331
|
+
try {
|
|
332
|
+
const raw = await promises.readFile(this.authorityFilePath(account), 'utf8');
|
|
333
|
+
const parsed = JSON.parse(raw);
|
|
334
|
+
return 1 === parsed.version && 'fallback' === parsed.authority;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (isNodeError(error) && 'ENOENT' === error.code) return false;
|
|
337
|
+
throw new CredentialStoreError(`读取登录凭证权威副本标记失败:${credential_store_errorMessage(error)}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async markFallbackAuthoritative(account) {
|
|
341
|
+
const record = {
|
|
342
|
+
version: 1,
|
|
343
|
+
authority: 'fallback'
|
|
344
|
+
};
|
|
345
|
+
await writePrivateFileAtomic(this.authorityFilePath(account), `${JSON.stringify(record, null, 2)}\n`);
|
|
346
|
+
}
|
|
347
|
+
async clearFallbackAuthority(account) {
|
|
348
|
+
try {
|
|
349
|
+
await promises.unlink(this.authorityFilePath(account));
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (!isNodeError(error) || 'ENOENT' !== error.code) throw new CredentialStoreError(`清理登录凭证权威副本标记失败:${credential_store_errorMessage(error)}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
masterKeyPath() {
|
|
355
|
+
return node_path.join(this.baseDir, MASTER_KEY_FILE);
|
|
356
|
+
}
|
|
357
|
+
credentialFilePath(account) {
|
|
358
|
+
const digest = node_crypto.createHash('sha256').update(account).digest('hex');
|
|
359
|
+
return node_path.join(this.baseDir, CREDENTIALS_DIR, `${digest}.json`);
|
|
360
|
+
}
|
|
361
|
+
authorityFilePath(account) {
|
|
362
|
+
const digest = node_crypto.createHash('sha256').update(account).digest('hex');
|
|
363
|
+
return node_path.join(this.baseDir, AUTHORITIES_DIR, `${digest}.json`);
|
|
364
|
+
}
|
|
365
|
+
async readMasterKey() {
|
|
366
|
+
try {
|
|
367
|
+
return parseMasterKey(await promises.readFile(this.masterKeyPath(), 'utf8'));
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (isNodeError(error) && 'ENOENT' === error.code) throw new MissingMasterKeyError();
|
|
370
|
+
if (error instanceof CredentialStoreError) throw error;
|
|
371
|
+
throw new CredentialStoreError(`无法读取用于解密本地登录凭证的密钥文件 master.key:${credential_store_errorMessage(error)}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async readOrCreateMasterKey() {
|
|
375
|
+
try {
|
|
376
|
+
return await this.readMasterKey();
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (!(error instanceof MissingMasterKeyError)) throw error;
|
|
379
|
+
}
|
|
380
|
+
const key = node_crypto.randomBytes(32);
|
|
381
|
+
try {
|
|
382
|
+
const created = await writePrivateFileCreateOnce(this.masterKeyPath(), `${key.toString('base64url')}\n`);
|
|
383
|
+
return created ? key : this.readMasterKey();
|
|
384
|
+
} catch (error) {
|
|
385
|
+
throw new CredentialStoreError(`无法保存用于加密本地登录凭证的密钥文件 master.key:${credential_store_errorMessage(error)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
class MacOSKeychainStore {
|
|
390
|
+
async get(account) {
|
|
391
|
+
try {
|
|
392
|
+
const { stdout } = await execFileAsync('security', [
|
|
393
|
+
'find-generic-password',
|
|
394
|
+
'-a',
|
|
395
|
+
account,
|
|
396
|
+
'-s',
|
|
397
|
+
SERVICE_NAME,
|
|
398
|
+
'-w'
|
|
399
|
+
]);
|
|
400
|
+
return String(stdout).replace(/\n$/, '');
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (isMacOSKeychainNotFound(error)) return;
|
|
403
|
+
throw new CredentialStoreError(`从 macOS 钥匙串读取登录凭证失败:${credential_store_errorMessage(error)}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async set(account, value) {
|
|
407
|
+
try {
|
|
408
|
+
await execFileAsync('security', [
|
|
409
|
+
'add-generic-password',
|
|
410
|
+
'-a',
|
|
411
|
+
account,
|
|
412
|
+
'-s',
|
|
413
|
+
SERVICE_NAME,
|
|
414
|
+
'-w',
|
|
415
|
+
value,
|
|
416
|
+
'-U'
|
|
417
|
+
]);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
throw new CredentialStoreError(`向 macOS 钥匙串写入登录凭证失败:${credential_store_errorMessage(error)}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async delete(account) {
|
|
423
|
+
try {
|
|
424
|
+
await execFileAsync('security', [
|
|
425
|
+
'delete-generic-password',
|
|
426
|
+
'-a',
|
|
427
|
+
account,
|
|
428
|
+
'-s',
|
|
429
|
+
SERVICE_NAME
|
|
430
|
+
]);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (isMacOSKeychainNotFound(error)) return;
|
|
433
|
+
throw new CredentialStoreError(`从 macOS 钥匙串删除登录凭证失败:${credential_store_errorMessage(error)}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
class SecretToolStore {
|
|
438
|
+
async get(account) {
|
|
439
|
+
try {
|
|
440
|
+
const { stdout } = await execFileAsync('secret-tool', [
|
|
441
|
+
'lookup',
|
|
442
|
+
'service',
|
|
443
|
+
SERVICE_NAME,
|
|
444
|
+
'account',
|
|
445
|
+
account
|
|
446
|
+
]);
|
|
447
|
+
return String(stdout).replace(/\n$/, '');
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (isSecretToolNotFound(error)) return;
|
|
450
|
+
throw new CredentialStoreError(`从 Secret Service 读取登录凭证失败:${credential_store_errorMessage(error)}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async set(account, value) {
|
|
454
|
+
try {
|
|
455
|
+
await execFileWithInput('secret-tool', [
|
|
456
|
+
'store',
|
|
457
|
+
'--label',
|
|
458
|
+
'Doubao Open CLI 登录凭证',
|
|
459
|
+
'service',
|
|
460
|
+
SERVICE_NAME,
|
|
461
|
+
'account',
|
|
462
|
+
account
|
|
463
|
+
], value);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
throw new CredentialStoreError(`向 Secret Service 写入登录凭证失败:${credential_store_errorMessage(error)}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async delete(account) {
|
|
469
|
+
try {
|
|
470
|
+
await execFileAsync('secret-tool', [
|
|
471
|
+
'clear',
|
|
472
|
+
'service',
|
|
473
|
+
SERVICE_NAME,
|
|
474
|
+
'account',
|
|
475
|
+
account
|
|
476
|
+
]);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (isSecretToolNotFound(error)) return;
|
|
479
|
+
throw new CredentialStoreError(`从 Secret Service 删除登录凭证失败:${credential_store_errorMessage(error)}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function execFileWithInput(command, args, input) {
|
|
484
|
+
return new Promise((resolve, reject)=>{
|
|
485
|
+
const child = spawn(command, args, {
|
|
486
|
+
stdio: [
|
|
487
|
+
'pipe',
|
|
488
|
+
'ignore',
|
|
489
|
+
'pipe'
|
|
490
|
+
]
|
|
491
|
+
});
|
|
492
|
+
let stderr = '';
|
|
493
|
+
child.stderr.setEncoding('utf8');
|
|
494
|
+
child.stderr.on('data', (chunk)=>{
|
|
495
|
+
stderr += String(chunk);
|
|
496
|
+
});
|
|
497
|
+
child.on('error', reject);
|
|
498
|
+
child.on('close', (code)=>{
|
|
499
|
+
if (0 === code) resolve();
|
|
500
|
+
else {
|
|
501
|
+
const error = new Error(stderr.trim() || `${command} 已退出,退出码为 ${code}`);
|
|
502
|
+
reject(error);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
child.stdin.end(input);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async function writePrivateFileAtomic(filePath, payload) {
|
|
509
|
+
const dir = node_path.dirname(filePath);
|
|
510
|
+
await promises.mkdir(dir, {
|
|
511
|
+
recursive: true,
|
|
512
|
+
mode: 448
|
|
513
|
+
});
|
|
514
|
+
if ('win32' !== process.platform) await promises.chmod(dir, 448);
|
|
515
|
+
const tempPath = node_path.join(dir, `.${node_path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
516
|
+
try {
|
|
517
|
+
await promises.writeFile(tempPath, payload, {
|
|
518
|
+
mode: 384,
|
|
519
|
+
flag: 'wx'
|
|
520
|
+
});
|
|
521
|
+
if ('win32' !== process.platform) await promises.chmod(tempPath, 384);
|
|
522
|
+
await promises.rename(tempPath, filePath);
|
|
523
|
+
if ('win32' !== process.platform) await promises.chmod(filePath, 384);
|
|
524
|
+
} finally{
|
|
525
|
+
try {
|
|
526
|
+
await promises.unlink(tempPath);
|
|
527
|
+
} catch {}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function writePrivateFileCreateOnce(filePath, payload) {
|
|
531
|
+
const dir = node_path.dirname(filePath);
|
|
532
|
+
await promises.mkdir(dir, {
|
|
533
|
+
recursive: true,
|
|
534
|
+
mode: 448
|
|
535
|
+
});
|
|
536
|
+
if ('win32' !== process.platform) await promises.chmod(dir, 448);
|
|
537
|
+
const tempPath = node_path.join(dir, `.${node_path.basename(filePath)}.${process.pid}.${Date.now()}.${node_crypto.randomBytes(8).toString('hex')}.tmp`);
|
|
538
|
+
try {
|
|
539
|
+
await promises.writeFile(tempPath, payload, {
|
|
540
|
+
mode: 384,
|
|
541
|
+
flag: 'wx'
|
|
542
|
+
});
|
|
543
|
+
if ('win32' !== process.platform) await promises.chmod(tempPath, 384);
|
|
544
|
+
try {
|
|
545
|
+
await promises.link(tempPath, filePath);
|
|
546
|
+
if ('win32' !== process.platform) await promises.chmod(filePath, 384);
|
|
547
|
+
return true;
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (isNodeError(error) && 'EEXIST' === error.code) return false;
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
} finally{
|
|
553
|
+
try {
|
|
554
|
+
await promises.unlink(tempPath);
|
|
555
|
+
} catch {}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function supportsCredentialAuthority(store) {
|
|
559
|
+
const candidate = store;
|
|
560
|
+
return 'function' == typeof candidate.isFallbackAuthoritative && 'function' == typeof candidate.markFallbackAuthoritative && 'function' == typeof candidate.clearFallbackAuthority;
|
|
561
|
+
}
|
|
562
|
+
function parseMasterKey(raw) {
|
|
563
|
+
const key = Buffer.from(raw.trim(), 'base64url');
|
|
564
|
+
if (32 !== key.length) throw new CredentialStoreError('无法读取本地登录凭证:密钥文件 master.key 的内容无法解析为 32 字节密钥。');
|
|
565
|
+
return key;
|
|
566
|
+
}
|
|
567
|
+
function isEncryptedCredentialRecord(value) {
|
|
568
|
+
if (!value || 'object' != typeof value) return false;
|
|
569
|
+
const record = value;
|
|
570
|
+
return record.version === LOCAL_STORE_VERSION && record.algorithm === LOCAL_STORE_ALGORITHM && 'string' == typeof record.iv && 'string' == typeof record.authTag && 'string' == typeof record.ciphertext;
|
|
571
|
+
}
|
|
572
|
+
function associatedData(account) {
|
|
573
|
+
return `${SERVICE_NAME}:${account}`;
|
|
574
|
+
}
|
|
575
|
+
function defaultEncryptedCredentialDir() {
|
|
576
|
+
if ('darwin' === process.platform) return node_path.join(node_os.homedir(), 'Library', 'Application Support', LOCAL_STORE_DIR_NAME);
|
|
577
|
+
if ('win32' === process.platform) return node_path.join(process.env.APPDATA || node_path.join(node_os.homedir(), 'AppData', 'Roaming'), LOCAL_STORE_DIR_NAME);
|
|
578
|
+
return node_path.join(process.env.XDG_DATA_HOME || node_path.join(node_os.homedir(), '.local', 'share'), LOCAL_STORE_DIR_NAME);
|
|
579
|
+
}
|
|
580
|
+
function isMacOSKeychainNotFound(error) {
|
|
581
|
+
const code = errorCode(error);
|
|
582
|
+
const stderr = errorStderr(error);
|
|
583
|
+
return (1 === code || 44 === code) && (stderr.includes('could not be found') || stderr.includes('The specified item could not be found'));
|
|
584
|
+
}
|
|
585
|
+
function isSecretToolNotFound(error) {
|
|
586
|
+
const code = errorCode(error);
|
|
587
|
+
const stderr = errorStderr(error);
|
|
588
|
+
return 1 === code && (stderr.includes('could not be found') || stderr.includes('No matching items') || '' === stderr.trim());
|
|
589
|
+
}
|
|
590
|
+
function errorCode(error) {
|
|
591
|
+
return 'object' == typeof error && null !== error && 'code' in error ? error.code : void 0;
|
|
592
|
+
}
|
|
593
|
+
function errorStderr(error) {
|
|
594
|
+
return 'object' == typeof error && null !== error && 'stderr' in error ? String(error.stderr ?? '') : '';
|
|
595
|
+
}
|
|
596
|
+
function isNodeError(error) {
|
|
597
|
+
return error instanceof Error && 'code' in error;
|
|
598
|
+
}
|
|
599
|
+
function credential_store_errorMessage(error) {
|
|
600
|
+
if ('object' == typeof error && null !== error && 'stderr' in error) {
|
|
601
|
+
const stderr = String(error.stderr ?? '').trim();
|
|
602
|
+
if (stderr) return stderr;
|
|
603
|
+
}
|
|
604
|
+
if (error instanceof Error && error.message) return error.message;
|
|
605
|
+
return String(error);
|
|
606
|
+
}
|
|
607
|
+
class AuthError extends Error {
|
|
608
|
+
constructor(message, code){
|
|
609
|
+
super(message);
|
|
610
|
+
this.code = code;
|
|
611
|
+
this.name = 'AuthError';
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
class NotLoggedInError extends AuthError {
|
|
615
|
+
constructor(message = '尚未登录,请运行 dbx auth login 完成登录。'){
|
|
616
|
+
super(message, 'NOT_LOGGED_IN');
|
|
617
|
+
this.name = 'NotLoggedInError';
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
class TokenExpiredError extends AuthError {
|
|
621
|
+
constructor(message = '登录凭证已过期,请运行 dbx auth login 重新登录。'){
|
|
622
|
+
super(message, 'TOKEN_EXPIRED');
|
|
623
|
+
this.name = 'TokenExpiredError';
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
class DeviceAuthError extends AuthError {
|
|
627
|
+
constructor(message, code = 'DEVICE_AUTH_FAILED', options = {}){
|
|
628
|
+
super(message, code);
|
|
629
|
+
this.name = 'DeviceAuthError';
|
|
630
|
+
this.logId = options.logId;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function decodeJwtPayload(token) {
|
|
634
|
+
const part = token.split('.')[1];
|
|
635
|
+
if (!part) return;
|
|
636
|
+
try {
|
|
637
|
+
const json = Buffer.from(part, 'base64url').toString('utf8');
|
|
638
|
+
const payload = JSON.parse(json);
|
|
639
|
+
return payload && 'object' == typeof payload ? payload : void 0;
|
|
640
|
+
} catch {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function getUidFromToken(token) {
|
|
645
|
+
const payload = decodeJwtPayload(token);
|
|
646
|
+
const uid = payload?.uid ?? payload?.sub;
|
|
647
|
+
if ('string' == typeof uid && uid.length > 0) return uid;
|
|
648
|
+
if ('number' == typeof uid && Number.isFinite(uid)) return String(uid);
|
|
649
|
+
}
|
|
650
|
+
const ENV_ACCESS_TOKEN_EXPIRES_AT = 4102444800;
|
|
651
|
+
async function readToken(tokenPath, options = {}) {
|
|
652
|
+
const envToken = readAccessTokenFromEnv();
|
|
653
|
+
if (envToken) return {
|
|
654
|
+
accessToken: envToken,
|
|
655
|
+
tokenType: 'Bearer',
|
|
656
|
+
expiresAt: ENV_ACCESS_TOKEN_EXPIRES_AT,
|
|
657
|
+
baseUrl: options.envTokenBaseUrl ?? ''
|
|
658
|
+
};
|
|
659
|
+
const raw = await readTokenFile(tokenPath);
|
|
660
|
+
if (void 0 === raw) return readLegacyPlaintextToken(tokenPath, options);
|
|
661
|
+
let parsed;
|
|
662
|
+
try {
|
|
663
|
+
parsed = JSON.parse(raw);
|
|
664
|
+
} catch (_error) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (isStoredTokenMetadata(parsed)) {
|
|
668
|
+
if (!options.allowExpired && isExpired(parsed)) return void await store_deleteToken(tokenPath, options);
|
|
669
|
+
const accessToken = await credentialStore(options).get(parsed.credentialAccount);
|
|
670
|
+
if (!accessToken) throw new CredentialStoreError("已找到登录记录,但登录凭证缺失或无法访问。请在能够访问凭证存储的终端中重新运行 dbx auth login。");
|
|
671
|
+
return {
|
|
672
|
+
accessToken,
|
|
673
|
+
tokenType: parsed.tokenType,
|
|
674
|
+
expiresAt: parsed.expiresAt,
|
|
675
|
+
baseUrl: parsed.baseUrl
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (!isStoredToken(parsed)) return;
|
|
679
|
+
if (!options.allowExpired && isExpired(parsed)) return void await store_deleteToken(tokenPath, options);
|
|
680
|
+
await writeToken(tokenPath, parsed, options);
|
|
681
|
+
return parsed;
|
|
682
|
+
}
|
|
683
|
+
async function writeToken(tokenPath, token, options = {}) {
|
|
684
|
+
const metadata = {
|
|
685
|
+
version: 2,
|
|
686
|
+
storage: 'keychain',
|
|
687
|
+
credentialAccount: credentialAccountForPath(tokenPath),
|
|
688
|
+
tokenType: token.tokenType,
|
|
689
|
+
expiresAt: token.expiresAt,
|
|
690
|
+
baseUrl: token.baseUrl
|
|
691
|
+
};
|
|
692
|
+
await credentialStore(options).set(metadata.credentialAccount, token.accessToken);
|
|
693
|
+
await promises.mkdir(node_path.dirname(tokenPath), {
|
|
694
|
+
recursive: true,
|
|
695
|
+
mode: 448
|
|
696
|
+
});
|
|
697
|
+
if ('win32' !== process.platform) await promises.chmod(node_path.dirname(tokenPath), 448);
|
|
698
|
+
const tempPath = `${tokenPath}.${process.pid}.${Date.now()}.tmp`;
|
|
699
|
+
await promises.writeFile(tempPath, `${JSON.stringify(metadata, null, 2)}\n`, {
|
|
700
|
+
mode: 384
|
|
701
|
+
});
|
|
702
|
+
if ('win32' !== process.platform) await promises.chmod(tempPath, 384);
|
|
703
|
+
await promises.rename(tempPath, tokenPath);
|
|
704
|
+
if ('win32' !== process.platform) await promises.chmod(tokenPath, 384);
|
|
705
|
+
}
|
|
706
|
+
async function store_deleteToken(tokenPath, options = {}) {
|
|
707
|
+
const raw = await readTokenFile(tokenPath);
|
|
708
|
+
let account = credentialAccountForPath(tokenPath);
|
|
709
|
+
if (void 0 !== raw) try {
|
|
710
|
+
const parsed = JSON.parse(raw);
|
|
711
|
+
if (isStoredTokenMetadata(parsed)) account = parsed.credentialAccount;
|
|
712
|
+
} catch {}
|
|
713
|
+
await credentialStore(options).delete(account);
|
|
714
|
+
try {
|
|
715
|
+
await promises.unlink(tokenPath);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
if (!store_isNodeError(error) || 'ENOENT' !== error.code) throw error;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function isExpired(token, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
721
|
+
return token.expiresAt <= nowSeconds;
|
|
722
|
+
}
|
|
723
|
+
function isStoredToken(value) {
|
|
724
|
+
if (!value || 'object' != typeof value) return false;
|
|
725
|
+
const token = value;
|
|
726
|
+
return 'string' == typeof token.accessToken && token.accessToken.length > 0 && 'string' == typeof token.tokenType && token.tokenType.length > 0 && 'number' == typeof token.expiresAt && Number.isFinite(token.expiresAt) && 'string' == typeof token.baseUrl && token.baseUrl.length > 0;
|
|
727
|
+
}
|
|
728
|
+
function isStoredTokenMetadata(value) {
|
|
729
|
+
if (!value || 'object' != typeof value) return false;
|
|
730
|
+
const metadata = value;
|
|
731
|
+
return 2 === metadata.version && 'keychain' === metadata.storage && 'string' == typeof metadata.credentialAccount && metadata.credentialAccount.length > 0 && 'string' == typeof metadata.tokenType && metadata.tokenType.length > 0 && 'number' == typeof metadata.expiresAt && Number.isFinite(metadata.expiresAt) && 'string' == typeof metadata.baseUrl && metadata.baseUrl.length > 0;
|
|
732
|
+
}
|
|
733
|
+
async function readTokenFile(tokenPath) {
|
|
734
|
+
try {
|
|
735
|
+
return await promises.readFile(tokenPath, 'utf8');
|
|
736
|
+
} catch (error) {
|
|
737
|
+
store_isNodeError(error) && error.code;
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function readLegacyPlaintextToken(tokenPath, options) {
|
|
742
|
+
if (!options.legacyTokenPath || node_path.resolve(options.legacyTokenPath) === node_path.resolve(tokenPath)) return;
|
|
743
|
+
const raw = await readTokenFile(options.legacyTokenPath);
|
|
744
|
+
if (void 0 === raw) return;
|
|
745
|
+
let parsed;
|
|
746
|
+
try {
|
|
747
|
+
parsed = JSON.parse(raw);
|
|
748
|
+
} catch {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (!isStoredToken(parsed)) return;
|
|
752
|
+
if (!options.allowExpired && isExpired(parsed)) return void await store_deleteToken(options.legacyTokenPath, options);
|
|
753
|
+
await writeToken(tokenPath, parsed, options);
|
|
754
|
+
await store_deleteToken(options.legacyTokenPath, options);
|
|
755
|
+
return parsed;
|
|
756
|
+
}
|
|
757
|
+
function credentialStore(options) {
|
|
758
|
+
return options.credentialStore ?? defaultCredentialStore();
|
|
759
|
+
}
|
|
760
|
+
function credentialAccountForPath(tokenPath) {
|
|
761
|
+
const digest = node_crypto.createHash('sha256').update(node_path.resolve(tokenPath)).digest('hex').slice(0, 32);
|
|
762
|
+
return `token:${digest}`;
|
|
763
|
+
}
|
|
764
|
+
function readAccessTokenFromEnv() {
|
|
765
|
+
const value = process.env.DBX_ACCESS_TOKEN?.trim();
|
|
766
|
+
return value ? value : void 0;
|
|
767
|
+
}
|
|
768
|
+
function store_isNodeError(error) {
|
|
769
|
+
return error instanceof Error && 'code' in error;
|
|
770
|
+
}
|
|
771
|
+
var types_TokenStatus;
|
|
772
|
+
(function(TokenStatus) {
|
|
773
|
+
TokenStatus[TokenStatus["Pending"] = 1] = "Pending";
|
|
774
|
+
TokenStatus[TokenStatus["Authorized"] = 2] = "Authorized";
|
|
775
|
+
TokenStatus[TokenStatus["Expired"] = 3] = "Expired";
|
|
776
|
+
TokenStatus[TokenStatus["Invalid"] = 4] = "Invalid";
|
|
777
|
+
TokenStatus[TokenStatus["SlowDown"] = 5] = "SlowDown";
|
|
778
|
+
})(types_TokenStatus || (types_TokenStatus = {}));
|
|
779
|
+
const ISSUE_DEVICE_CODE_PATH = '/app_developer/oauth/device/code';
|
|
780
|
+
const EXCHANGE_DEVICE_TOKEN_PATH = '/app_developer/oauth/token';
|
|
781
|
+
async function login(options = {}) {
|
|
782
|
+
const resolved = config_resolveOptions(options);
|
|
783
|
+
const fetchImpl = resolved.fetch ?? fetch;
|
|
784
|
+
const device = await requestDeviceCode(fetchImpl, resolved.baseUrl, resolved.requestHeaders);
|
|
785
|
+
const verificationUrl = buildVerificationUrl(resolved.verificationUrl, device.user_code);
|
|
786
|
+
resolved.onVerification?.({
|
|
787
|
+
userCode: device.user_code,
|
|
788
|
+
verificationUrl,
|
|
789
|
+
expiresIn: device.expires_in,
|
|
790
|
+
interval: device.interval
|
|
791
|
+
});
|
|
792
|
+
return pollDeviceToken(device.device_code, resolved, Math.max(1, device.interval), 1000 * device.expires_in);
|
|
793
|
+
}
|
|
794
|
+
async function startLogin(options = {}) {
|
|
795
|
+
const resolved = config_resolveOptions(options);
|
|
796
|
+
const fetchImpl = resolved.fetch ?? fetch;
|
|
797
|
+
const device = await requestDeviceCode(fetchImpl, resolved.baseUrl, resolved.requestHeaders);
|
|
798
|
+
const verificationUrl = buildVerificationUrl(resolved.verificationUrl, device.user_code);
|
|
799
|
+
resolved.onVerification?.({
|
|
800
|
+
userCode: device.user_code,
|
|
801
|
+
verificationUrl,
|
|
802
|
+
expiresIn: device.expires_in,
|
|
803
|
+
interval: device.interval
|
|
804
|
+
});
|
|
805
|
+
return {
|
|
806
|
+
userCode: device.user_code,
|
|
807
|
+
deviceCode: device.device_code,
|
|
808
|
+
verificationUrl,
|
|
809
|
+
expiresIn: device.expires_in,
|
|
810
|
+
interval: device.interval
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
async function completeLogin(deviceCode, options = {}) {
|
|
814
|
+
return pollDeviceToken(deviceCode, config_resolveOptions(options), 5, 600000);
|
|
815
|
+
}
|
|
816
|
+
async function pollDeviceToken(deviceCode, resolved, initialInterval, expiresInMs) {
|
|
817
|
+
const fetchImpl = resolved.fetch ?? fetch;
|
|
818
|
+
const sleep = resolved.sleep ?? promises_setTimeout;
|
|
819
|
+
let intervalSeconds = initialInterval;
|
|
820
|
+
const deadline = Date.now() + expiresInMs;
|
|
821
|
+
while(Date.now() < deadline){
|
|
822
|
+
await sleep(1000 * intervalSeconds);
|
|
823
|
+
const token = await requestToken(fetchImpl, resolved.baseUrl, deviceCode, resolved.requestHeaders);
|
|
824
|
+
resolved.onPoll?.({
|
|
825
|
+
status: token.status,
|
|
826
|
+
interval: intervalSeconds
|
|
827
|
+
});
|
|
828
|
+
if (token.status === types_TokenStatus.Pending) continue;
|
|
829
|
+
if (token.status === types_TokenStatus.SlowDown) {
|
|
830
|
+
intervalSeconds += initialInterval;
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (token.status === types_TokenStatus.Expired) throw new DeviceAuthError('设备授权已过期,请运行 dbx auth login 重新登录。', 'DEVICE_AUTH_EXPIRED');
|
|
834
|
+
if (token.status === types_TokenStatus.Invalid) throw new DeviceAuthError('设备授权无效,请运行 dbx auth login 重新登录。', 'DEVICE_AUTH_INVALID');
|
|
835
|
+
if (token.status === types_TokenStatus.Authorized) {
|
|
836
|
+
if (!token.access_token) throw new DeviceAuthError('无法完成登录:认证服务已确认授权,但未返回 access_token。');
|
|
837
|
+
const stored = toStoredToken(token, resolved.baseUrl);
|
|
838
|
+
await writeToken(resolved.tokenPath, stored, {
|
|
839
|
+
credentialStore: resolved.credentialStore
|
|
840
|
+
});
|
|
841
|
+
return {
|
|
842
|
+
accessToken: stored.accessToken,
|
|
843
|
+
tokenType: stored.tokenType,
|
|
844
|
+
expiresAt: stored.expiresAt,
|
|
845
|
+
baseUrl: stored.baseUrl,
|
|
846
|
+
uid: getUidFromToken(stored.accessToken),
|
|
847
|
+
tokenPath: resolved.tokenPath
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
throw new DeviceAuthError(`无法完成登录:认证服务返回了无法识别的授权状态 status=${String(token.status)}`);
|
|
851
|
+
}
|
|
852
|
+
throw new DeviceAuthError('等待设备授权超时,请运行 dbx auth login 重新发起登录。', 'DEVICE_AUTH_EXPIRED');
|
|
853
|
+
}
|
|
854
|
+
async function requestDeviceCode(fetchImpl, baseUrl, requestHeaders) {
|
|
855
|
+
const response = await fetchImpl(`${baseUrl}${ISSUE_DEVICE_CODE_PATH}`, {
|
|
856
|
+
method: 'POST',
|
|
857
|
+
headers: withJsonHeaders(requestHeaders),
|
|
858
|
+
body: JSON.stringify({
|
|
859
|
+
client_name: DEFAULT_CLIENT_NAME
|
|
860
|
+
})
|
|
861
|
+
});
|
|
862
|
+
const data = await readJson(response);
|
|
863
|
+
const payload = unwrapData(data);
|
|
864
|
+
if (!payload || !payload.device_code || !payload.user_code || 'number' != typeof payload.expires_in || 'number' != typeof payload.interval) throw new DeviceAuthError(`无法获取登录授权码:认证服务返回的数据缺少必要字段或字段类型不正确(HTTP ${response.status}):${JSON.stringify(data)}`);
|
|
865
|
+
return payload;
|
|
866
|
+
}
|
|
867
|
+
async function requestToken(fetchImpl, baseUrl, deviceCode, requestHeaders) {
|
|
868
|
+
const response = await fetchImpl(`${baseUrl}${EXCHANGE_DEVICE_TOKEN_PATH}`, {
|
|
869
|
+
method: 'POST',
|
|
870
|
+
headers: withJsonHeaders(requestHeaders),
|
|
871
|
+
body: JSON.stringify({
|
|
872
|
+
device_code: deviceCode
|
|
873
|
+
})
|
|
874
|
+
});
|
|
875
|
+
const data = await readJson(response);
|
|
876
|
+
const payload = unwrapData(data);
|
|
877
|
+
if (payload?.access_token) return {
|
|
878
|
+
status: types_TokenStatus.Authorized,
|
|
879
|
+
access_token: payload.access_token,
|
|
880
|
+
token_type: payload.token_type,
|
|
881
|
+
expires_in: payload.expires_in,
|
|
882
|
+
expires_at: payload.expires_at
|
|
883
|
+
};
|
|
884
|
+
if (!payload || 'number' != typeof payload.status) throw new DeviceAuthError(`无法获取登录凭证:认证服务返回的数据缺少有效的 status 字段(HTTP ${response.status}):${JSON.stringify(data)}`);
|
|
885
|
+
return payload;
|
|
886
|
+
}
|
|
887
|
+
async function readJson(response) {
|
|
888
|
+
const headerLogId = response.headers.get('x-tt-logid') ?? void 0;
|
|
889
|
+
let data;
|
|
890
|
+
try {
|
|
891
|
+
data = await response.json();
|
|
892
|
+
} catch {
|
|
893
|
+
throw new DeviceAuthError(`无法读取登录结果:认证服务返回的内容不是有效的 JSON(HTTP ${response.status})${formatLogId(headerLogId)}`, 'DEVICE_AUTH_FAILED', {
|
|
894
|
+
logId: headerLogId
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
const bodyLogId = extractLogId(data);
|
|
898
|
+
const logId = bodyLogId ?? headerLogId;
|
|
899
|
+
if (!response.ok) throw new DeviceAuthError(`认证服务返回 HTTP ${response.status}:${JSON.stringify(data)}${formatLogId(logId)}`, 'DEVICE_AUTH_FAILED', {
|
|
900
|
+
logId
|
|
901
|
+
});
|
|
902
|
+
const apiError = extractApiError(data);
|
|
903
|
+
if (apiError) throw new DeviceAuthError(`认证服务出错(错误码 ${apiError.code}):${apiError.message}${formatLogId(apiError.logId ?? logId)}`, 'DEVICE_AUTH_FAILED', {
|
|
904
|
+
logId: apiError.logId ?? logId
|
|
905
|
+
});
|
|
906
|
+
return data;
|
|
907
|
+
}
|
|
908
|
+
function extractApiError(data) {
|
|
909
|
+
if (data && 'object' == typeof data) {
|
|
910
|
+
const d = data;
|
|
911
|
+
if ('number' == typeof d.code && 0 !== d.code) return {
|
|
912
|
+
code: d.code,
|
|
913
|
+
message: 'string' == typeof d.message ? d.message : '未知错误',
|
|
914
|
+
logId: 'string' == typeof d.log_id ? d.log_id : void 0
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
function extractLogId(data) {
|
|
920
|
+
if (!data || 'object' != typeof data) return;
|
|
921
|
+
const d = data;
|
|
922
|
+
return 'string' == typeof d.log_id && d.log_id ? d.log_id : void 0;
|
|
923
|
+
}
|
|
924
|
+
function formatLogId(logId) {
|
|
925
|
+
return logId ? ` [log_id: ${logId}]` : '';
|
|
926
|
+
}
|
|
927
|
+
function unwrapData(data) {
|
|
928
|
+
if (data && 'object' == typeof data && 'data' in data) return data.data;
|
|
929
|
+
return data;
|
|
930
|
+
}
|
|
931
|
+
function withJsonHeaders(requestHeaders) {
|
|
932
|
+
const headers = new Headers(requestHeaders);
|
|
933
|
+
headers.set('Content-Type', 'application/json');
|
|
934
|
+
return headers;
|
|
935
|
+
}
|
|
936
|
+
function toStoredToken(response, baseUrl) {
|
|
937
|
+
const now = Math.floor(Date.now() / 1000);
|
|
938
|
+
return {
|
|
939
|
+
accessToken: response.access_token ?? '',
|
|
940
|
+
tokenType: response.token_type ?? 'Bearer',
|
|
941
|
+
expiresAt: 'number' == typeof response.expires_at ? response.expires_at : now + (response.expires_in ?? 0),
|
|
942
|
+
baseUrl
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function buildVerificationUrl(verificationUrl, userCode) {
|
|
946
|
+
const url = new URL(verificationUrl);
|
|
947
|
+
url.searchParams.set('user_code', userCode);
|
|
948
|
+
return url.toString();
|
|
949
|
+
}
|
|
950
|
+
function getAuthHeaders(input, headers, options = {}) {
|
|
951
|
+
return resolveAuthHeaders(input, headers, config_resolveOptions(options));
|
|
952
|
+
}
|
|
953
|
+
async function resolveAuthHeaders(input, initHeaders, resolved) {
|
|
954
|
+
assertAllowedAuthenticatedRequest(input, resolved.allowedOrigins);
|
|
955
|
+
const token = await readToken(resolved.tokenPath, {
|
|
956
|
+
allowExpired: true,
|
|
957
|
+
envTokenBaseUrl: resolved.baseUrl,
|
|
958
|
+
credentialStore: resolved.credentialStore,
|
|
959
|
+
legacyTokenPath: resolved.legacyTokenPath
|
|
960
|
+
});
|
|
961
|
+
if (!token) throw new NotLoggedInError();
|
|
962
|
+
if (isExpired(token)) throw new TokenExpiredError();
|
|
963
|
+
const headers = createMergedHeaders(input, initHeaders);
|
|
964
|
+
resolved.requestHeaders.forEach((value, key)=>{
|
|
965
|
+
headers.set(key, value);
|
|
966
|
+
});
|
|
967
|
+
headers.set('Authorization', `${token.tokenType || 'Bearer'} ${token.accessToken}`);
|
|
968
|
+
return headers;
|
|
969
|
+
}
|
|
970
|
+
function createMergedHeaders(input, initHeaders) {
|
|
971
|
+
const headers = new Headers(input instanceof Request ? input.headers : void 0);
|
|
972
|
+
new Headers(initHeaders).forEach((value, key)=>{
|
|
973
|
+
headers.set(key, value);
|
|
974
|
+
});
|
|
975
|
+
return headers;
|
|
976
|
+
}
|
|
977
|
+
function assertAllowedAuthenticatedRequest(input, allowedOrigins) {
|
|
978
|
+
const url = requestUrl(input);
|
|
979
|
+
if ('https:' !== url.protocol) throw new Error(`携带登录凭证的请求必须使用 HTTPS:${url.toString()}`);
|
|
980
|
+
if (!allowedOrigins.includes(url.origin)) throw new Error(`无法向不受信任的来源发送 Authorization 请求头:${url.origin}`);
|
|
981
|
+
}
|
|
982
|
+
function requestUrl(input) {
|
|
983
|
+
try {
|
|
984
|
+
if ('string' == typeof input) return new URL(input);
|
|
985
|
+
if (input instanceof URL) return input;
|
|
986
|
+
return new URL(input.url);
|
|
987
|
+
} catch {
|
|
988
|
+
throw new Error('携带登录凭证的请求必须使用完整的 HTTPS URL');
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
async function whoami(options = {}) {
|
|
992
|
+
const resolved = config_resolveOptions(options);
|
|
993
|
+
let token;
|
|
994
|
+
try {
|
|
995
|
+
token = await readToken(resolved.tokenPath, {
|
|
996
|
+
envTokenBaseUrl: resolved.baseUrl,
|
|
997
|
+
credentialStore: resolved.credentialStore,
|
|
998
|
+
legacyTokenPath: resolved.legacyTokenPath
|
|
999
|
+
});
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
if (error instanceof CredentialStoreError) return {
|
|
1002
|
+
loggedIn: false,
|
|
1003
|
+
tokenPath: resolved.tokenPath,
|
|
1004
|
+
baseUrl: resolved.baseUrl,
|
|
1005
|
+
credentialError: {
|
|
1006
|
+
code: 'CREDENTIAL_STORE_UNAVAILABLE',
|
|
1007
|
+
message: error.message
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
throw error;
|
|
1011
|
+
}
|
|
1012
|
+
if (!token) return {
|
|
1013
|
+
loggedIn: false,
|
|
1014
|
+
tokenPath: resolved.tokenPath
|
|
1015
|
+
};
|
|
1016
|
+
const user = await fetchCurrentUser(options.fetch ?? fetch, resolved.baseUrl, token, resolved.requestHeaders);
|
|
1017
|
+
if (!user) return {
|
|
1018
|
+
loggedIn: false,
|
|
1019
|
+
expiresAt: token.expiresAt,
|
|
1020
|
+
tokenPath: resolved.tokenPath,
|
|
1021
|
+
baseUrl: resolved.baseUrl
|
|
1022
|
+
};
|
|
1023
|
+
return {
|
|
1024
|
+
loggedIn: true,
|
|
1025
|
+
uid: user.userId,
|
|
1026
|
+
name: user.name,
|
|
1027
|
+
avatar: user.avatar,
|
|
1028
|
+
expiresAt: token.expiresAt,
|
|
1029
|
+
tokenPath: resolved.tokenPath,
|
|
1030
|
+
baseUrl: resolved.baseUrl
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
async function fetchCurrentUser(fetchImpl, baseUrl, token, requestHeaders) {
|
|
1034
|
+
const headers = new Headers(requestHeaders);
|
|
1035
|
+
headers.set('Authorization', `${token.tokenType || 'Bearer'} ${token.accessToken}`);
|
|
1036
|
+
const response = await fetchImpl(`${baseUrl.replace(/\/+$/, '')}/app_developer/api/user/basic`, {
|
|
1037
|
+
method: 'GET',
|
|
1038
|
+
headers
|
|
1039
|
+
});
|
|
1040
|
+
if (!response.ok) return;
|
|
1041
|
+
const result = await response.json();
|
|
1042
|
+
if (0 !== result.code) return;
|
|
1043
|
+
const userId = toStringId(result.data?.user_id);
|
|
1044
|
+
return {
|
|
1045
|
+
userId,
|
|
1046
|
+
name: result.data?.name,
|
|
1047
|
+
avatar: result.data?.avatar
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
function toStringId(value) {
|
|
1051
|
+
if ('string' == typeof value && value.length > 0) return value;
|
|
1052
|
+
if ('number' == typeof value && Number.isFinite(value)) return String(value);
|
|
1053
|
+
}
|
|
1054
|
+
const baseURL = `${process.env.DBX_BUILD_API_BASE_URL || runtime.apiUrl}/platform/api/v1`;
|
|
1055
|
+
const http_request = ofetch.create({
|
|
1056
|
+
baseURL: baseURL,
|
|
1057
|
+
headers: runtime.apiHeaders,
|
|
1058
|
+
method: 'GET',
|
|
1059
|
+
timeout: 30000,
|
|
1060
|
+
retry: 0,
|
|
1061
|
+
redirect: 'error',
|
|
1062
|
+
responseType: 'json',
|
|
1063
|
+
ignoreResponseError: true,
|
|
1064
|
+
async onRequest ({ request, options }) {
|
|
1065
|
+
const target = 'string' == typeof request ? new URL(request, options.baseURL) : request;
|
|
1066
|
+
options.headers = await getAuthHeaders(target, options.headers, {
|
|
1067
|
+
baseUrl: options.baseURL
|
|
1068
|
+
});
|
|
1069
|
+
if (options.signal && options.timeout) options.signal = AbortSignal.any([
|
|
1070
|
+
options.signal,
|
|
1071
|
+
AbortSignal.timeout(options.timeout)
|
|
1072
|
+
]);
|
|
1073
|
+
},
|
|
1074
|
+
onResponse ({ response }) {
|
|
1075
|
+
const result = response._data;
|
|
1076
|
+
if (!response.ok || result?.code !== 0) {
|
|
1077
|
+
const message = result?.msg || result?.message || result?.statusText || `接口请求失败(HTTP ${response.status})`;
|
|
1078
|
+
const details = [];
|
|
1079
|
+
if (response.headers.has('x-tt-logid')) details.push(`请求日志 ID:${response.headers.get('x-tt-logid')}`);
|
|
1080
|
+
throw new Error([
|
|
1081
|
+
message,
|
|
1082
|
+
'',
|
|
1083
|
+
...details
|
|
1084
|
+
].join('\n'));
|
|
1085
|
+
}
|
|
1086
|
+
response._data = result.data;
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
async function createDeviceTicket(params, signal) {
|
|
1090
|
+
const data = await http_request(`/app_developer/check_api/apps/${encodeURIComponent(params.appKey)}/debug/ticket`, {
|
|
1091
|
+
baseURL: runtime.deviceApiUrl,
|
|
1092
|
+
method: 'POST',
|
|
1093
|
+
signal,
|
|
1094
|
+
body: {
|
|
1095
|
+
debug_context: JSON.stringify({
|
|
1096
|
+
appletName: params.appKey,
|
|
1097
|
+
appletHost: params.hostUrl,
|
|
1098
|
+
appletType: 0,
|
|
1099
|
+
isSigned: false,
|
|
1100
|
+
isEncrypted: false,
|
|
1101
|
+
defaultDebugTagState: true,
|
|
1102
|
+
wifiConnectUrl: params.wifiConnectUrl,
|
|
1103
|
+
...params.versionTag ? {
|
|
1104
|
+
versionTag: params.versionTag
|
|
1105
|
+
} : {}
|
|
1106
|
+
})
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
const ticket = z.object({
|
|
1110
|
+
debug_token: z.string().trim().min(1),
|
|
1111
|
+
encrypted_debug_payload: z.string().trim().min(1)
|
|
1112
|
+
}).parse(data);
|
|
1113
|
+
return {
|
|
1114
|
+
debugToken: ticket.debug_token,
|
|
1115
|
+
qrCodeValue: JSON.stringify({
|
|
1116
|
+
action: 'start_real_device_debug',
|
|
1117
|
+
...ticket
|
|
1118
|
+
})
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function getAppInfo(params, signal) {
|
|
1122
|
+
return http_request('/devtool/plugin-mini-app-base-info', {
|
|
1123
|
+
query: {
|
|
1124
|
+
...params
|
|
1125
|
+
},
|
|
1126
|
+
signal
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
function uploadPackage(fileName, packageContent, signal) {
|
|
1130
|
+
return http_request('/devtool/plugin-package-uploads', {
|
|
1131
|
+
method: 'POST',
|
|
1132
|
+
query: {
|
|
1133
|
+
file_name: fileName
|
|
1134
|
+
},
|
|
1135
|
+
headers: {
|
|
1136
|
+
'Content-Type': 'application/zip'
|
|
1137
|
+
},
|
|
1138
|
+
body: packageContent,
|
|
1139
|
+
signal
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
function validateMaterial(params) {
|
|
1143
|
+
return http_request('/devtool/plugin-material-validations', {
|
|
1144
|
+
method: 'POST',
|
|
1145
|
+
body: params
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
function listPluginCategories() {
|
|
1149
|
+
return http_request('/devtool/plugin-categories');
|
|
1150
|
+
}
|
|
1151
|
+
function getPluginDraft(name) {
|
|
1152
|
+
return http_request('/devtool/plugin-drafts', {
|
|
1153
|
+
query: {
|
|
1154
|
+
name
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
function upsertPluginDraft(params) {
|
|
1159
|
+
return http_request('/devtool/plugin-drafts', {
|
|
1160
|
+
method: 'POST',
|
|
1161
|
+
body: params
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
function savePluginVersion(params) {
|
|
1165
|
+
return http_request('/devtool/plugin-versions', {
|
|
1166
|
+
method: 'POST',
|
|
1167
|
+
body: params
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
async function listPluginVersions(plugin, signal) {
|
|
1171
|
+
const identity = plugin.id ? {
|
|
1172
|
+
ability_id: plugin.id
|
|
1173
|
+
} : {
|
|
1174
|
+
name: plugin.name
|
|
1175
|
+
};
|
|
1176
|
+
const versions = [];
|
|
1177
|
+
const tokens = new Set();
|
|
1178
|
+
let pageToken;
|
|
1179
|
+
do {
|
|
1180
|
+
signal?.throwIfAborted();
|
|
1181
|
+
const result = await http_request('/devtool/plugin-versions', {
|
|
1182
|
+
query: {
|
|
1183
|
+
...identity,
|
|
1184
|
+
page_size: 50,
|
|
1185
|
+
page_token: pageToken
|
|
1186
|
+
},
|
|
1187
|
+
signal
|
|
1188
|
+
});
|
|
1189
|
+
versions.push(...result.items ?? []);
|
|
1190
|
+
pageToken = result.page?.next_page_token;
|
|
1191
|
+
if (pageToken && tokens.has(pageToken)) throw new Error('版本列表分页异常,请稍后重试');
|
|
1192
|
+
if (pageToken) tokens.add(pageToken);
|
|
1193
|
+
}while (pageToken);
|
|
1194
|
+
return versions.sort((left, right)=>(right.version ?? '').localeCompare(left.version ?? '', 'en', {
|
|
1195
|
+
numeric: true
|
|
1196
|
+
}));
|
|
1197
|
+
}
|
|
1198
|
+
function getBuildStatus(params, signal) {
|
|
1199
|
+
return http_request('/devtool/plugin-versions/build-status', {
|
|
1200
|
+
query: {
|
|
1201
|
+
...params
|
|
1202
|
+
},
|
|
1203
|
+
signal
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
function createPreview(params, signal) {
|
|
1207
|
+
return http_request('/devtool/plugin-previews', {
|
|
1208
|
+
method: 'POST',
|
|
1209
|
+
body: params,
|
|
1210
|
+
signal
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
function updateUserPlugin(params, signal) {
|
|
1214
|
+
return http_request('/devtool/plugin-operations', {
|
|
1215
|
+
method: 'POST',
|
|
1216
|
+
body: params,
|
|
1217
|
+
signal
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
function submitApproval(name, version) {
|
|
1221
|
+
return http_request('/devtool/plugin-approvals/submit', {
|
|
1222
|
+
method: 'POST',
|
|
1223
|
+
body: {
|
|
1224
|
+
name,
|
|
1225
|
+
version
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
function withdrawApproval(approvalId) {
|
|
1230
|
+
return http_request('/devtool/plugin-approvals/withdraw', {
|
|
1231
|
+
method: 'POST',
|
|
1232
|
+
body: {
|
|
1233
|
+
approval_id: approvalId
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
const PLUGIN_MANIFEST_FILENAME = 'plugin.json';
|
|
1238
|
+
const MCP_CONFIG_FILENAME = 'mcp.json';
|
|
1239
|
+
const SKILL_FILENAME = 'SKILL.md';
|
|
1240
|
+
const APP_MANIFEST_FILENAME = 'manifest.yaml';
|
|
1241
|
+
const DOUBAO_APP_DIRECTORY = 'com.doubao/app';
|
|
1242
|
+
const APP_GENERATED_DIRECTORIES = [
|
|
1243
|
+
'dist',
|
|
1244
|
+
'node_modules/.doubao'
|
|
1245
|
+
];
|
|
1246
|
+
const PROJECT_EXCLUDED_DIRECTORIES = [
|
|
1247
|
+
'.git',
|
|
1248
|
+
'.dbx'
|
|
1249
|
+
];
|
|
1250
|
+
const PROJECT_EXCLUDED_FILES = [
|
|
1251
|
+
'.gitkeep',
|
|
1252
|
+
'.gitignore',
|
|
1253
|
+
'.DS_Store'
|
|
1254
|
+
];
|
|
1255
|
+
const TEMPLATES_DIRECTORY = 'templates';
|
|
1256
|
+
const STANDARD_TEMPLATE_DIRECTORY = 'standard';
|
|
1257
|
+
const TEMPLATE_SUFFIX = '.template';
|
|
1258
|
+
const PLUGIN_NAME_PLACEHOLDER = '__PLUGIN_NAME__';
|
|
1259
|
+
const PACKAGE_CACHE_DIRECTORY = '.dbx/tools';
|
|
1260
|
+
const SKILL_NAME = 'doubao-plugin-cli';
|
|
1261
|
+
const APP_SKILL_NAME = 'doubao-plugin-app';
|
|
1262
|
+
const PROJECT_SKILL_PATH = `.agents/skills/${SKILL_NAME}`;
|
|
1263
|
+
function isInteractive() {
|
|
1264
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1265
|
+
}
|
|
1266
|
+
function writeStdout(message) {
|
|
1267
|
+
console.log(stripVTControlCharacters(message));
|
|
1268
|
+
}
|
|
1269
|
+
function writeStderr(message) {
|
|
1270
|
+
console.error(stripVTControlCharacters(message));
|
|
1271
|
+
}
|
|
1272
|
+
const terminal_log = isInteractive() ? log : {
|
|
1273
|
+
info: writeStdout,
|
|
1274
|
+
step: writeStdout,
|
|
1275
|
+
success: writeStdout,
|
|
1276
|
+
warn: writeStderr,
|
|
1277
|
+
error: writeStderr
|
|
1278
|
+
};
|
|
1279
|
+
function resolveEntry(require, spec) {
|
|
1280
|
+
try {
|
|
1281
|
+
const manifestPath = require.resolve(`${spec.name}/package.json`);
|
|
1282
|
+
const manifest = JSON.parse(node_fs.readFileSync(manifestPath, 'utf8'));
|
|
1283
|
+
if (manifest.version !== spec.version) return;
|
|
1284
|
+
const exported = manifest.exports?.[spec.entry];
|
|
1285
|
+
const relativeEntry = 'string' == typeof exported ? exported : exported?.import;
|
|
1286
|
+
if ('string' != typeof relativeEntry || !relativeEntry) return;
|
|
1287
|
+
const entry = node_path.resolve(node_path.dirname(manifestPath), relativeEntry);
|
|
1288
|
+
if (node_fs.existsSync(entry)) return entry;
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
if ('MODULE_NOT_FOUND' !== error.code) throw error;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async function loadPackage(spec, signal, output) {
|
|
1294
|
+
signal.throwIfAborted();
|
|
1295
|
+
const cache = node_path.join(homedir(), PACKAGE_CACHE_DIRECTORY, spec.name, spec.version);
|
|
1296
|
+
const requireCache = createRequire(node_path.join(cache, 'package.json'));
|
|
1297
|
+
let entry = resolveEntry(createRequire(import.meta.url), spec) ?? resolveEntry(requireCache, spec);
|
|
1298
|
+
if (!entry) {
|
|
1299
|
+
node_fs.mkdirSync(node_path.dirname(cache), {
|
|
1300
|
+
recursive: true
|
|
1301
|
+
});
|
|
1302
|
+
const staging = node_fs.mkdtempSync(`${cache}-`);
|
|
1303
|
+
const label = spec.name === runtime.kit.name ? '应用编译与模拟器工具' : '应用开发工具';
|
|
1304
|
+
const progress = output || !isInteractive() ? void 0 : ora({
|
|
1305
|
+
text: ` 正在安装${label}`,
|
|
1306
|
+
stream: process.stdout,
|
|
1307
|
+
discardStdin: false
|
|
1308
|
+
}).start();
|
|
1309
|
+
try {
|
|
1310
|
+
if (!progress) (output?.log ?? terminal_log).info(`正在安装${label}`);
|
|
1311
|
+
const child = spawn('npm', [
|
|
1312
|
+
'install',
|
|
1313
|
+
`${spec.name}@${spec.version}`,
|
|
1314
|
+
'--prefix',
|
|
1315
|
+
staging,
|
|
1316
|
+
'--registry',
|
|
1317
|
+
runtime.registry,
|
|
1318
|
+
'--no-audit',
|
|
1319
|
+
'--no-fund',
|
|
1320
|
+
'--loglevel',
|
|
1321
|
+
'error'
|
|
1322
|
+
], {
|
|
1323
|
+
stdio: [
|
|
1324
|
+
'ignore',
|
|
1325
|
+
'ignore',
|
|
1326
|
+
'pipe'
|
|
1327
|
+
],
|
|
1328
|
+
signal
|
|
1329
|
+
});
|
|
1330
|
+
let installError = '';
|
|
1331
|
+
child.stderr.on('data', (data)=>{
|
|
1332
|
+
installError += data.toString();
|
|
1333
|
+
});
|
|
1334
|
+
const [code] = await once(child, 'close');
|
|
1335
|
+
if (0 !== code) throw new Error(`${spec.name} 安装失败,退出码:${code}\n${installError.trim()}`);
|
|
1336
|
+
signal.throwIfAborted();
|
|
1337
|
+
if (!resolveEntry(createRequire(node_path.join(staging, 'package.json')), spec)) throw new Error(`${spec.name} 的版本或入口不正确`);
|
|
1338
|
+
try {
|
|
1339
|
+
node_fs.renameSync(staging, cache);
|
|
1340
|
+
} catch {
|
|
1341
|
+
if (!resolveEntry(requireCache, spec)) {
|
|
1342
|
+
node_fs.rmSync(cache, {
|
|
1343
|
+
recursive: true,
|
|
1344
|
+
force: true
|
|
1345
|
+
});
|
|
1346
|
+
node_fs.renameSync(staging, cache);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
entry = resolveEntry(requireCache, spec);
|
|
1350
|
+
progress?.succeed(` ${label}已安装`);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
progress?.fail(signal.aborted ? ` 已取消安装${label}` : ` ${label}安装失败`);
|
|
1353
|
+
signal.throwIfAborted();
|
|
1354
|
+
throw error;
|
|
1355
|
+
} finally{
|
|
1356
|
+
node_fs.rmSync(staging, {
|
|
1357
|
+
recursive: true,
|
|
1358
|
+
force: true
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
signal.throwIfAborted();
|
|
1363
|
+
if (!entry) throw new Error(`${spec.name} 的入口不存在`);
|
|
1364
|
+
return pathToFileURL(entry).href;
|
|
1365
|
+
}
|
|
1366
|
+
function withProgress(message, action) {
|
|
1367
|
+
if (!isInteractive()) {
|
|
1368
|
+
terminal_log.info(message);
|
|
1369
|
+
return action();
|
|
1370
|
+
}
|
|
1371
|
+
return oraPromise(action, {
|
|
1372
|
+
text: ` ${message}`,
|
|
1373
|
+
successText: ` ${message}完成`,
|
|
1374
|
+
stream: process.stdout,
|
|
1375
|
+
discardStdin: false
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
let installing;
|
|
1379
|
+
async function loadAppModule(entry, signal = new AbortController().signal, output) {
|
|
1380
|
+
const main = await (installing ??= loadPackage(runtime.appDev, signal, output));
|
|
1381
|
+
signal.throwIfAborted();
|
|
1382
|
+
const url = '.' === entry ? main : await loadPackage({
|
|
1383
|
+
...runtime.appDev,
|
|
1384
|
+
entry
|
|
1385
|
+
}, signal, output);
|
|
1386
|
+
return import(url);
|
|
1387
|
+
}
|
|
1388
|
+
const appPlugin = {
|
|
1389
|
+
name: 'doubao-app',
|
|
1390
|
+
setup (api) {
|
|
1391
|
+
api.onAddCommand(({ program })=>{
|
|
1392
|
+
program.commands.find((command)=>'dev' === command.name()).option('--app', '同时启动应用调试(App)');
|
|
1393
|
+
});
|
|
1394
|
+
api.onDev(()=>[
|
|
1395
|
+
{
|
|
1396
|
+
name: 'app',
|
|
1397
|
+
title: '应用调试(App)',
|
|
1398
|
+
option: 'app',
|
|
1399
|
+
watch: [
|
|
1400
|
+
MCP_CONFIG_FILENAME
|
|
1401
|
+
],
|
|
1402
|
+
appliesTo: ({ project })=>Boolean(project.app),
|
|
1403
|
+
async start (ctx) {
|
|
1404
|
+
const { startApp } = await loadAppModule('.', ctx.signal, ctx);
|
|
1405
|
+
ctx.signal.throwIfAborted();
|
|
1406
|
+
await startApp(ctx, {
|
|
1407
|
+
kitVersion: runtime.kit.version,
|
|
1408
|
+
quiet: !ctx.interactive,
|
|
1409
|
+
loadKit: (output)=>loadPackage(runtime.kit, ctx.signal, output),
|
|
1410
|
+
queryAppInfo: getAppInfo,
|
|
1411
|
+
requestTicket: createDeviceTicket,
|
|
1412
|
+
queryVersions: ()=>listPluginVersions(ctx.project.plugin, ctx.signal)
|
|
1413
|
+
});
|
|
1414
|
+
},
|
|
1415
|
+
async createUI (ctx, onExit) {
|
|
1416
|
+
const { createAppUI } = await loadAppModule('./ui', ctx.signal);
|
|
1417
|
+
ctx.signal.throwIfAborted();
|
|
1418
|
+
return createAppUI({
|
|
1419
|
+
name: ctx.project.plugin.name,
|
|
1420
|
+
version: runtime.version,
|
|
1421
|
+
metadata: {
|
|
1422
|
+
Kit: runtime.kit.version
|
|
1423
|
+
},
|
|
1424
|
+
onExit
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
]);
|
|
1429
|
+
api.onModifyPackagePlan((_ctx, plan)=>({
|
|
1430
|
+
...plan,
|
|
1431
|
+
exclude: [
|
|
1432
|
+
...plan.exclude ?? [],
|
|
1433
|
+
`${DOUBAO_APP_DIRECTORY}/node_modules`
|
|
1434
|
+
]
|
|
1435
|
+
}));
|
|
1436
|
+
api.onAfterUpload(async (ctx)=>{
|
|
1437
|
+
if (!ctx.project.app) return;
|
|
1438
|
+
const { version } = await withProgress('保存预览版本', ()=>savePluginVersion({
|
|
1439
|
+
...ctx.project.plugin.id ? {
|
|
1440
|
+
ability_id: ctx.project.plugin.id
|
|
1441
|
+
} : {
|
|
1442
|
+
name: ctx.project.plugin.name
|
|
1443
|
+
},
|
|
1444
|
+
request_id: randomUUID()
|
|
1445
|
+
}));
|
|
1446
|
+
if (!version) throw new Error('服务端未返回保存的版本号,请到平台草稿页查看。');
|
|
1447
|
+
const { showPreview } = await loadAppModule('./preview');
|
|
1448
|
+
await showPreview(ctx.project, version, {
|
|
1449
|
+
withProgress: withProgress,
|
|
1450
|
+
queryBuildStatus: getBuildStatus,
|
|
1451
|
+
createPreview: createPreview
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
function findProjectRoot(cwd) {
|
|
1457
|
+
let root = external_node_path_resolve(cwd);
|
|
1458
|
+
while(!existsSync(join(root, PLUGIN_MANIFEST_FILENAME))){
|
|
1459
|
+
const parent = dirname(root);
|
|
1460
|
+
if (parent === root) throw new Error(`未找到 plugin.json,请在应用插件项目内运行:${cwd}`);
|
|
1461
|
+
root = parent;
|
|
1462
|
+
}
|
|
1463
|
+
return root;
|
|
1464
|
+
}
|
|
1465
|
+
function findApp(projectRoot) {
|
|
1466
|
+
const root = join(projectRoot, DOUBAO_APP_DIRECTORY);
|
|
1467
|
+
if (existsSync(join(root, APP_MANIFEST_FILENAME)) && findProjectRoot(root) === projectRoot) return {
|
|
1468
|
+
root
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
const projectNameMessage = '项目名称须为 1 至 64 个小写字母、数字、连字符或点号,以字母或数字开头结尾,且不能包含连续连字符或连续点号。';
|
|
1472
|
+
const projectNameSchema = z.string().max(64, projectNameMessage).regex(/^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/, projectNameMessage);
|
|
1473
|
+
const versionSchema = z.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/, '版本号须为 x.y.z 格式,不能包含前导零');
|
|
1474
|
+
const jsonObjectSchema = z.record(z.string(), z.unknown());
|
|
1475
|
+
const pluginManifestSchema = z.looseObject({
|
|
1476
|
+
$schema: z.string().optional(),
|
|
1477
|
+
name: projectNameSchema,
|
|
1478
|
+
id: z.string().optional(),
|
|
1479
|
+
displayName: z.string().optional(),
|
|
1480
|
+
description: z.string().optional(),
|
|
1481
|
+
extensions: jsonObjectSchema.optional()
|
|
1482
|
+
});
|
|
1483
|
+
const mcpConfigSchema = z.looseObject({
|
|
1484
|
+
mcpServers: z.record(z.string(), jsonObjectSchema)
|
|
1485
|
+
});
|
|
1486
|
+
function currentSkillsRoot(packageName = runtime.name) {
|
|
1487
|
+
const source = packageName.replace(/^@/, '').replace(/[\\/]/g, '+');
|
|
1488
|
+
return join(homedir(), '.dbx', 'skills', source, 'current');
|
|
1489
|
+
}
|
|
1490
|
+
function refreshSkills() {
|
|
1491
|
+
const bundledRoot = external_node_path_resolve(import.meta.dirname, 'skills');
|
|
1492
|
+
if (!__rspack_external_node_fs_1b05aee1.statSync(join(bundledRoot, SKILL_NAME, 'SKILL.md')).isFile()) throw new Error('CLI 安装包中的开发 Skill 不完整,请重新安装 CLI。');
|
|
1493
|
+
replaceDirectoryLink(bundledRoot, currentSkillsRoot());
|
|
1494
|
+
}
|
|
1495
|
+
async function syncProjectSkill(cwd) {
|
|
1496
|
+
const projectRoot = findProjectRoot(cwd);
|
|
1497
|
+
pluginManifestSchema.parse(readJsonSync(join(projectRoot, 'plugin.json')));
|
|
1498
|
+
const hasApp = Boolean(findApp(projectRoot));
|
|
1499
|
+
const sources = [
|
|
1500
|
+
[
|
|
1501
|
+
SKILL_NAME,
|
|
1502
|
+
currentSkillsRoot()
|
|
1503
|
+
]
|
|
1504
|
+
];
|
|
1505
|
+
if (hasApp) {
|
|
1506
|
+
const { skillsRoot } = await loadAppModule('./assets');
|
|
1507
|
+
const appSkillsRoot = currentSkillsRoot(runtime.appDev.name);
|
|
1508
|
+
replaceDirectoryLink(skillsRoot, appSkillsRoot);
|
|
1509
|
+
sources.push([
|
|
1510
|
+
APP_SKILL_NAME,
|
|
1511
|
+
appSkillsRoot
|
|
1512
|
+
]);
|
|
1513
|
+
}
|
|
1514
|
+
for (const [name, source] of sources){
|
|
1515
|
+
const skillPath = `.agents/skills/${name}`;
|
|
1516
|
+
replaceDirectoryLink(join(source, name), join(projectRoot, skillPath));
|
|
1517
|
+
const ignorePath = join(projectRoot, '.gitignore');
|
|
1518
|
+
const ignore = __rspack_external_node_fs_1b05aee1.existsSync(ignorePath) ? __rspack_external_node_fs_1b05aee1.readFileSync(ignorePath, 'utf8') : '';
|
|
1519
|
+
const pattern = `/${skillPath}`;
|
|
1520
|
+
if (!ignore.split(/\r?\n/).includes(pattern)) appendText(ignorePath, ignore, `${pattern}\n`);
|
|
1521
|
+
}
|
|
1522
|
+
const guidePath = join(projectRoot, 'AGENTS.md');
|
|
1523
|
+
const guide = __rspack_external_node_fs_1b05aee1.existsSync(guidePath) ? __rspack_external_node_fs_1b05aee1.readFileSync(guidePath, 'utf8') : '';
|
|
1524
|
+
if (!guide.includes(`${PROJECT_SKILL_PATH}/SKILL.md`)) {
|
|
1525
|
+
const template = __rspack_external_node_fs_1b05aee1.readFileSync(join(import.meta.dirname, 'templates/standard/AGENTS.md'), 'utf8');
|
|
1526
|
+
appendText(guidePath, guide, template);
|
|
1527
|
+
}
|
|
1528
|
+
const appSkillPath = `.agents/skills/${APP_SKILL_NAME}/SKILL.md`;
|
|
1529
|
+
if (hasApp && !guide.includes(appSkillPath)) appendText(guidePath, __rspack_external_node_fs_1b05aee1.readFileSync(guidePath, 'utf8'), `\n仅开发豆包 App 的 Page、Widget 或 Manifest 时,先阅读 [应用开发 Skill](${appSkillPath})。普通 MCP 和 MCP Apps 不使用 App Manifest。\n`);
|
|
1530
|
+
}
|
|
1531
|
+
function appendText(path, existing, text) {
|
|
1532
|
+
const separator = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
1533
|
+
__rspack_external_node_fs_1b05aee1.writeFileSync(path, `${existing}${separator}${text}`);
|
|
1534
|
+
}
|
|
1535
|
+
function replaceDirectoryLink(source, target) {
|
|
1536
|
+
const existing = __rspack_external_node_fs_1b05aee1.lstatSync(target, {
|
|
1537
|
+
throwIfNoEntry: false
|
|
1538
|
+
});
|
|
1539
|
+
if (existing?.isSymbolicLink() && external_node_path_resolve(dirname(target), __rspack_external_node_fs_1b05aee1.readlinkSync(target)) === source) return;
|
|
1540
|
+
if (existing && !existing.isSymbolicLink()) throw new Error(`开发 Skill 路径已存在且不是软链接,请先确认并移走该路径:${target}`);
|
|
1541
|
+
__rspack_external_node_fs_1b05aee1.mkdirSync(dirname(target), {
|
|
1542
|
+
recursive: true
|
|
1543
|
+
});
|
|
1544
|
+
__rspack_external_node_fs_1b05aee1.rmSync(target, {
|
|
1545
|
+
recursive: true,
|
|
1546
|
+
force: true
|
|
1547
|
+
});
|
|
1548
|
+
__rspack_external_node_fs_1b05aee1.symlinkSync(source, target, 'win32' === process.platform ? 'junction' : 'dir');
|
|
1549
|
+
}
|
|
1550
|
+
const builtinPlugin = {
|
|
1551
|
+
name: 'builtin',
|
|
1552
|
+
setup (api) {
|
|
1553
|
+
api.onAddCommand(({ program, hooks })=>{
|
|
1554
|
+
const cwdOption = new Option('--cwd <path>', '项目根目录').argParser((value)=>external_node_path_resolve(value)).default(process.cwd());
|
|
1555
|
+
program.command('init').description('创建应用插件项目').argument('[name]', '项目名称,同时作为目录名').option('--app', '添加应用前端(App)').option('--no-install-deps', '创建 App 后不安装前端依赖').action(async (name, options)=>{
|
|
1556
|
+
const { runInit } = await import("./command~3.js");
|
|
1557
|
+
return runInit(name, options);
|
|
1558
|
+
});
|
|
1559
|
+
program.command('upload').description('上传应用插件包到开放平台').addOption(cwdOption).option('--version <version>', '应用插件版本,格式为 x.y.z').action(withProject(async (project, options)=>{
|
|
1560
|
+
const { runUpload } = await import("./command~4.js");
|
|
1561
|
+
await runUpload(project, options, hooks);
|
|
1562
|
+
}));
|
|
1563
|
+
program.command('dev').addOption(new Option('--plugin', '仅调试 Skill / MCP').conflicts('app')).option('--no-tui', '使用普通日志,不询问、不显示交互界面或自动发起登录').description('启动本地开发调试,监听目录变化并同步插件开发包').addOption(cwdOption).action(withProject(async (project, options)=>{
|
|
1564
|
+
const { runSession } = await import("./session.js");
|
|
1565
|
+
await runSession(project, options, hooks.dev);
|
|
1566
|
+
}));
|
|
1567
|
+
program.command('uninstall').description('从当前用户卸载插件开发包').addOption(cwdOption).action(withProject(async (project)=>{
|
|
1568
|
+
const { runUninstall } = await import("./uninstall.js");
|
|
1569
|
+
await runUninstall(project);
|
|
1570
|
+
}));
|
|
1571
|
+
program.command('categories').description('查询审核类目,输出包含 category_id 和 name 的 JSON 列表').action(async ()=>{
|
|
1572
|
+
const { runCategories } = await import("./categories.js");
|
|
1573
|
+
await runCategories();
|
|
1574
|
+
});
|
|
1575
|
+
program.command('audit').description('完善应用插件草稿、保存版本并提交审核').option('--input <path>', '审核材料 JSON 文件;相对路径按项目根目录解析,跳过交互提示').addOption(cwdOption).action(withProject(async (project, options)=>{
|
|
1576
|
+
const { runAudit } = await import("./command.js");
|
|
1577
|
+
await runAudit(project, options);
|
|
1578
|
+
}));
|
|
1579
|
+
program.command('cancel-audit').description('取消应用插件提审').addOption(cwdOption).action(withProject(async (project)=>{
|
|
1580
|
+
const { runCancelAudit } = await import("./command~2.js");
|
|
1581
|
+
await runCancelAudit(project);
|
|
1582
|
+
}));
|
|
1583
|
+
program.command('skills').description('配置项目开发 Skill').addOption(cwdOption).action(async ({ cwd })=>{
|
|
1584
|
+
await syncProjectSkill(cwd);
|
|
1585
|
+
terminal_log.success('项目开发 Skill 已配置');
|
|
1586
|
+
});
|
|
1587
|
+
const auth = program.command('auth').description('认证管理');
|
|
1588
|
+
auth.command('login').description('登录').addOption(new Option('--begin', '发起设备授权并显示操作说明').conflicts('complete')).addOption(new Option('--complete <device-code>', '使用指定设备码完成授权').conflicts('begin')).action(async (options)=>{
|
|
1589
|
+
const { runLogin } = await import("./command~1.js");
|
|
1590
|
+
await runLogin(options);
|
|
1591
|
+
});
|
|
1592
|
+
auth.command('status').description('查看登录状态').action(async ()=>{
|
|
1593
|
+
const { runStatus } = await import("./command~1.js");
|
|
1594
|
+
await runStatus();
|
|
1595
|
+
});
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
function withProject(run) {
|
|
1600
|
+
return async ({ cwd, ...options })=>{
|
|
1601
|
+
const { PluginProject } = await import("./plugin.js");
|
|
1602
|
+
await run(new PluginProject(cwd), options);
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
const platformService = {
|
|
1606
|
+
name: 'platform',
|
|
1607
|
+
title: '插件调试(Skill / MCP)',
|
|
1608
|
+
watch: [
|
|
1609
|
+
"skills",
|
|
1610
|
+
MCP_CONFIG_FILENAME,
|
|
1611
|
+
PLUGIN_MANIFEST_FILENAME
|
|
1612
|
+
],
|
|
1613
|
+
appliesTo: ()=>true,
|
|
1614
|
+
async start (ctx) {
|
|
1615
|
+
const sync = async ()=>{
|
|
1616
|
+
try {
|
|
1617
|
+
ctx.report({
|
|
1618
|
+
status: 'pending',
|
|
1619
|
+
message: '正在打包开发项目'
|
|
1620
|
+
});
|
|
1621
|
+
const content = await ctx.project.pack({
|
|
1622
|
+
exclude: [
|
|
1623
|
+
DOUBAO_APP_DIRECTORY
|
|
1624
|
+
]
|
|
1625
|
+
});
|
|
1626
|
+
ctx.signal.throwIfAborted();
|
|
1627
|
+
ctx.report({
|
|
1628
|
+
status: 'pending',
|
|
1629
|
+
message: '正在上传开发包'
|
|
1630
|
+
});
|
|
1631
|
+
const { package_id } = await uploadPackage(`${ctx.project.plugin.name}-${Date.now()}.zip`, content, ctx.signal);
|
|
1632
|
+
if (!package_id) throw new Error('上传失败:服务端未返回 package_id');
|
|
1633
|
+
ctx.signal.throwIfAborted();
|
|
1634
|
+
ctx.report({
|
|
1635
|
+
status: 'pending',
|
|
1636
|
+
message: '正在安装应用插件到当前用户'
|
|
1637
|
+
});
|
|
1638
|
+
await updateUserPlugin({
|
|
1639
|
+
package_id,
|
|
1640
|
+
action: 'Install'
|
|
1641
|
+
}, ctx.signal);
|
|
1642
|
+
ctx.report({
|
|
1643
|
+
status: 'success',
|
|
1644
|
+
message: '已同步'
|
|
1645
|
+
});
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
if (!ctx.signal.aborted) {
|
|
1648
|
+
ctx.report({
|
|
1649
|
+
status: 'error',
|
|
1650
|
+
message: '同步失败'
|
|
1651
|
+
});
|
|
1652
|
+
ctx.log.error(`平台同步失败:${errorMessage(error)}`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
ctx.onFileChange(sync);
|
|
1657
|
+
await sync();
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
const platformPlugin = {
|
|
1661
|
+
name: 'platform',
|
|
1662
|
+
setup (api) {
|
|
1663
|
+
api.onDev(()=>[
|
|
1664
|
+
platformService
|
|
1665
|
+
]);
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
async function main_main(argv = process.argv, options = {}) {
|
|
1669
|
+
const dbx = new Command().name('dbx').description(runtime.description).version(runtime.version, '-V, --version').enablePositionalOptions().configureOutput({
|
|
1670
|
+
outputError: (message)=>terminal_log.error(message.trimEnd())
|
|
1671
|
+
}).exitOverride();
|
|
1672
|
+
try {
|
|
1673
|
+
await applyPlugins(dbx, [
|
|
1674
|
+
builtinPlugin,
|
|
1675
|
+
platformPlugin,
|
|
1676
|
+
appPlugin,
|
|
1677
|
+
...options.plugins ?? []
|
|
1678
|
+
]);
|
|
1679
|
+
dbx.action(()=>dbx.outputHelp());
|
|
1680
|
+
refreshSkills();
|
|
1681
|
+
await dbx.parseAsync(argv);
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
if (error instanceof CommanderError) {
|
|
1684
|
+
process.exitCode = error.exitCode;
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
terminal_log.error(error instanceof Error ? error.message : String(error));
|
|
1688
|
+
process.exitCode = 1;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
export { APP_GENERATED_DIRECTORIES, DOUBAO_APP_DIRECTORY, MCP_CONFIG_FILENAME, NotLoggedInError, PLUGIN_MANIFEST_FILENAME, PLUGIN_NAME_PLACEHOLDER, PROJECT_EXCLUDED_DIRECTORIES, PROJECT_EXCLUDED_FILES, SKILL_FILENAME, STANDARD_TEMPLATE_DIRECTORY, TEMPLATES_DIRECTORY, TEMPLATE_SUFFIX, callHook, completeLogin, errorMessage, findApp, findProjectRoot, getPluginDraft, isInteractive, listPluginCategories, listPluginVersions, loadAppModule, loadPackage, login, main_main as main, mcpConfigSchema, pluginManifestSchema, projectNameSchema, runtime, savePluginVersion, startLogin, submitApproval, syncProjectSkill, terminal_log, updateUserPlugin, uploadPackage, upsertPluginDraft, validateMaterial, versionSchema, whoami, withProgress, withdrawApproval };
|