qdmp-cli 0.1.24 → 0.1.26
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/README.md +32 -0
- package/api.js +91 -6
- package/index.js +15 -0
- package/openapi.js +82 -0
- package/package.json +3 -2
- package/utils/authorized.js +4 -3
- package/utils/common.js +22 -0
- package/utils/openapiManifest.js +128 -0
package/README.md
CHANGED
|
@@ -61,3 +61,35 @@ qdmp-cli upload -e dev //上传开发环境
|
|
|
61
61
|
qdmp-cli upload -d "新增订单查询功能" //指定版本描述
|
|
62
62
|
```
|
|
63
63
|
直接运行上传命令时,会交互式提示输入选填的版本描述(200字以内)。
|
|
64
|
+
|
|
65
|
+
### 生成 OpenAPI 权限申请地址
|
|
66
|
+
|
|
67
|
+
Agent 在项目根目录维护 `.qdmp/openapi-usage.json`,只记录代码实际调用的 OpenAPI:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"schemaVersion": 1,
|
|
72
|
+
"openapis": [
|
|
73
|
+
{ "method": "GET", "path": "/spu/v1/detail" },
|
|
74
|
+
{ "showCode": "tag.search" }
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
生成申请地址:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
qdmp-cli openapi apply-url
|
|
83
|
+
qdmp-cli openapi apply-url --env dev --json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
命令会分页复用 `GET /qdmp-web/v1/developer/capabilities?type=openapi` 获取完整目录,将清单严格解析为
|
|
87
|
+
资源 ID,并排除已开通、审核中或无需权限的接口。需要手动申请的 ID 会生成 16 位短令牌,最终地址形如:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
https://open.qiandao.com/apps/<appId>/capabilities?requiredApis=<16位queryKey>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
本地联调可设置 `QDMP_API_BASE_URL=http://127.0.0.1:8082`,并通过
|
|
94
|
+
`--open-platform-url http://local.qiandao.com:8081` 指向本地前端。
|
|
95
|
+
CI 或隔离测试环境可通过 `QDMP_TOKEN` 临时注入登录 Token;日常交互仍优先使用 `qdmp-cli login`。
|
package/api.js
CHANGED
|
@@ -5,6 +5,14 @@ import { promisify } from "util";
|
|
|
5
5
|
|
|
6
6
|
const readFile = promisify(fs.readFile);
|
|
7
7
|
|
|
8
|
+
const resolveAPIBaseURL = (env) => {
|
|
9
|
+
const override = process.env.QDMP_API_BASE_URL?.trim().replace(/\/+$/, "");
|
|
10
|
+
if (override) return override;
|
|
11
|
+
return env === "prod"
|
|
12
|
+
? "https://api.qiandao.com"
|
|
13
|
+
: "https://dev-api.qiandao.com";
|
|
14
|
+
};
|
|
15
|
+
|
|
8
16
|
// API 基础配置
|
|
9
17
|
const API_CONFIG = {
|
|
10
18
|
timeout: 10000,
|
|
@@ -28,10 +36,7 @@ export const request = async (
|
|
|
28
36
|
needAuth = true
|
|
29
37
|
) => {
|
|
30
38
|
const config = {
|
|
31
|
-
baseURL:
|
|
32
|
-
env === "prod"
|
|
33
|
-
? "https://api.qiandao.com"
|
|
34
|
-
: "https://dev-api.qiandao.com",
|
|
39
|
+
baseURL: resolveAPIBaseURL(env),
|
|
35
40
|
method: "GET",
|
|
36
41
|
headers: { ...API_CONFIG.headers },
|
|
37
42
|
...options,
|
|
@@ -39,9 +44,10 @@ export const request = async (
|
|
|
39
44
|
// 添加认证头(如果需要且如果有 token)
|
|
40
45
|
if (needAuth) {
|
|
41
46
|
const token = getToken();
|
|
42
|
-
if (token) {
|
|
43
|
-
|
|
47
|
+
if (!token) {
|
|
48
|
+
throw new Error("未授权,执行 qdmp-cli login 完成登录");
|
|
44
49
|
}
|
|
50
|
+
config.headers["authorization"] = `Bearer ${token}`;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
try {
|
|
@@ -76,6 +82,85 @@ export const request = async (
|
|
|
76
82
|
}
|
|
77
83
|
};
|
|
78
84
|
|
|
85
|
+
const assertSuccessResponse = (result, action) => {
|
|
86
|
+
const code = Number(result?.code || 0);
|
|
87
|
+
if (Number.isFinite(code) && code !== 0) {
|
|
88
|
+
throw new Error(result?.message || `${action}失败`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 复用开发者能力列表接口,分页获取当前应用的完整 OpenAPI 目录。
|
|
94
|
+
*/
|
|
95
|
+
export const getAllOpenAPIResources = async (
|
|
96
|
+
appId,
|
|
97
|
+
env = "prod",
|
|
98
|
+
{ pageSize = 200, maxPages = 100 } = {},
|
|
99
|
+
) => {
|
|
100
|
+
const resources = [];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
let offset = 0;
|
|
103
|
+
|
|
104
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
105
|
+
const query = new URLSearchParams({
|
|
106
|
+
app_id: appId,
|
|
107
|
+
type: "openapi",
|
|
108
|
+
offset: String(offset),
|
|
109
|
+
limit: String(pageSize),
|
|
110
|
+
});
|
|
111
|
+
const result = await request(
|
|
112
|
+
`/qdmp-web/v1/developer/capabilities?${query.toString()}`,
|
|
113
|
+
{},
|
|
114
|
+
env,
|
|
115
|
+
);
|
|
116
|
+
assertSuccessResponse(result, "查询 OpenAPI 列表");
|
|
117
|
+
const pageResources = Array.isArray(result?.data?.resources)
|
|
118
|
+
? result.data.resources
|
|
119
|
+
: [];
|
|
120
|
+
let added = 0;
|
|
121
|
+
for (const item of pageResources) {
|
|
122
|
+
const resourceId = String(item?.resource?.resourceId || "");
|
|
123
|
+
if (!resourceId || seen.has(resourceId)) continue;
|
|
124
|
+
seen.add(resourceId);
|
|
125
|
+
resources.push(item);
|
|
126
|
+
added += 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const total = Number(result?.data?.total || 0);
|
|
130
|
+
offset += pageResources.length;
|
|
131
|
+
if (
|
|
132
|
+
pageResources.length === 0 ||
|
|
133
|
+
pageResources.length < pageSize ||
|
|
134
|
+
(Number.isFinite(total) && total > 0 && offset >= total) ||
|
|
135
|
+
added === 0
|
|
136
|
+
) {
|
|
137
|
+
return resources;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error(`OpenAPI 列表超过安全分页上限(${maxPages} 页)`);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const createOpenAPIApplicationLink = async (
|
|
145
|
+
appId,
|
|
146
|
+
resourceIds,
|
|
147
|
+
env = "prod",
|
|
148
|
+
) => {
|
|
149
|
+
const result = await request(
|
|
150
|
+
`/qdmp-web/v1/developer/apps/${encodeURIComponent(appId)}/openapi-application-links`,
|
|
151
|
+
{
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: JSON.stringify({ resourceIds }),
|
|
154
|
+
},
|
|
155
|
+
env,
|
|
156
|
+
);
|
|
157
|
+
assertSuccessResponse(result, "生成 OpenAPI 权限申请链接");
|
|
158
|
+
if (!result?.data?.queryKey || !Array.isArray(result?.data?.resourceIds)) {
|
|
159
|
+
throw new Error("生成 OpenAPI 权限申请链接失败:返回数据不完整");
|
|
160
|
+
}
|
|
161
|
+
return result.data;
|
|
162
|
+
};
|
|
163
|
+
|
|
79
164
|
/**
|
|
80
165
|
* 用户登录
|
|
81
166
|
* @param {string} username - 用户名
|
package/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
buildAction,
|
|
17
17
|
} from "./actions.js";
|
|
18
18
|
import { printLogo } from "./utils/logHandler.js";
|
|
19
|
+
import { openAPIApplyURLAction } from "./openapi.js";
|
|
19
20
|
// import.meta.url: esm模块化中的属性,获取模块文件的绝对地址
|
|
20
21
|
// __dirname: 获取模块文件所在目录的绝对路径,由nodejs在每个模块注入的特殊变量,只能在commonjs中使用
|
|
21
22
|
// process.cwd(): 获取当前执行nodejs命令的当前工作目录
|
|
@@ -69,4 +70,18 @@ program
|
|
|
69
70
|
.option("-e, --env <env>", "当前环境")
|
|
70
71
|
.option("-d, --description <description>", "版本描述(选填,200字以内)")
|
|
71
72
|
.action(uploadAction);
|
|
73
|
+
|
|
74
|
+
const openapi = program
|
|
75
|
+
.command("openapi")
|
|
76
|
+
.description("OpenAPI 使用清单与权限申请工具");
|
|
77
|
+
|
|
78
|
+
openapi
|
|
79
|
+
.command("apply-url")
|
|
80
|
+
.description("根据代码使用清单生成权限申请页面地址")
|
|
81
|
+
.option("-m, --manifest <path>", "OpenAPI 使用清单", ".qdmp/openapi-usage.json")
|
|
82
|
+
.option("-e, --env <env>", "环境:prod 或 dev", "prod")
|
|
83
|
+
.option("-a, --app-id <appId>", "应用 ID")
|
|
84
|
+
.option("--open-platform-url <url>", "开放平台前端地址(本地联调使用)")
|
|
85
|
+
.option("--json", "输出结构化 JSON")
|
|
86
|
+
.action(openAPIApplyURLAction);
|
|
72
87
|
program.parse(process.argv);
|
package/openapi.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createOpenAPIApplicationLink, getAllOpenAPIResources } from "./api.js";
|
|
2
|
+
import { resolveAppId } from "./utils/common.js";
|
|
3
|
+
import {
|
|
4
|
+
classifyOpenAPIResources,
|
|
5
|
+
readOpenAPIManifest,
|
|
6
|
+
resolveOpenAPIManifest,
|
|
7
|
+
} from "./utils/openapiManifest.js";
|
|
8
|
+
import { error, info, success, warn } from "./utils/logHandler.js";
|
|
9
|
+
|
|
10
|
+
const openPlatformBaseURL = (env, override) => {
|
|
11
|
+
const baseURL = override?.trim().replace(/\/+$/, "")
|
|
12
|
+
|| (env === "prod" ? "https://open.qiandao.com" : "https://dev-open.qiandao.com");
|
|
13
|
+
return baseURL;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const prepareOpenAPIApplication = async ({
|
|
17
|
+
appId,
|
|
18
|
+
env = "prod",
|
|
19
|
+
manifest = ".qdmp/openapi-usage.json",
|
|
20
|
+
openPlatformUrl,
|
|
21
|
+
cwd = process.cwd(),
|
|
22
|
+
}) => {
|
|
23
|
+
const resolvedAppId = String(appId || resolveAppId(cwd, env)).trim();
|
|
24
|
+
if (!resolvedAppId) {
|
|
25
|
+
throw new Error("未找到 appId,请使用 --app-id 指定,或检查 qdmp-config.json/frontend/qdmp.json");
|
|
26
|
+
}
|
|
27
|
+
const usage = readOpenAPIManifest(manifest, cwd);
|
|
28
|
+
const catalog = await getAllOpenAPIResources(resolvedAppId, env);
|
|
29
|
+
const matched = resolveOpenAPIManifest(usage.openapis, catalog);
|
|
30
|
+
const classified = classifyOpenAPIResources(matched);
|
|
31
|
+
|
|
32
|
+
let queryKey = "";
|
|
33
|
+
let expiresAt = 0;
|
|
34
|
+
if (classified.required.length > 0) {
|
|
35
|
+
const link = await createOpenAPIApplicationLink(resolvedAppId, classified.required, env);
|
|
36
|
+
queryKey = link.queryKey;
|
|
37
|
+
expiresAt = Number(link.expiresAt || 0);
|
|
38
|
+
if (link.resourceIds.map(String).join(",") !== classified.required.join(",")) {
|
|
39
|
+
throw new Error("短链返回的 OpenAPI ID 与请求不一致,已停止生成申请地址");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const baseURL = openPlatformBaseURL(env, openPlatformUrl);
|
|
44
|
+
const url = new URL(`/apps/${encodeURIComponent(resolvedAppId)}/capabilities`, `${baseURL}/`);
|
|
45
|
+
if (queryKey) url.searchParams.set("requiredApis", queryKey);
|
|
46
|
+
return {
|
|
47
|
+
appId: resolvedAppId,
|
|
48
|
+
manifest: usage.path,
|
|
49
|
+
matchedResourceIds: matched.map((item) => String(item.resource.resourceId)),
|
|
50
|
+
requiredResourceIds: classified.required,
|
|
51
|
+
grantedResourceIds: classified.granted,
|
|
52
|
+
pendingResourceIds: classified.pending,
|
|
53
|
+
noPermissionRequiredResourceIds: classified.noPermissionRequired,
|
|
54
|
+
unavailableResourceIds: classified.unavailable,
|
|
55
|
+
queryKey,
|
|
56
|
+
expiresAt,
|
|
57
|
+
url: url.toString(),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const openAPIApplyURLAction = async (option) => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await prepareOpenAPIApplication(option);
|
|
64
|
+
if (option.json) {
|
|
65
|
+
console.log(JSON.stringify(result, null, 2));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
info(`已匹配 ${result.matchedResourceIds.length} 个 OpenAPI`);
|
|
69
|
+
if (result.grantedResourceIds.length > 0) info(`${result.grantedResourceIds.length} 个已开通`);
|
|
70
|
+
if (result.pendingResourceIds.length > 0) info(`${result.pendingResourceIds.length} 个正在审核`);
|
|
71
|
+
if (result.noPermissionRequiredResourceIds.length > 0) info(`${result.noPermissionRequiredResourceIds.length} 个无需申请`);
|
|
72
|
+
if (result.unavailableResourceIds.length > 0) warn(`${result.unavailableResourceIds.length} 个当前不可申请`);
|
|
73
|
+
if (result.requiredResourceIds.length === 0) {
|
|
74
|
+
success("当前没有需要手动申请的 OpenAPI");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
success(`已生成权限申请地址(${result.requiredResourceIds.length} 个接口):`);
|
|
78
|
+
console.log(result.url);
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
error(cause?.message || String(cause));
|
|
81
|
+
}
|
|
82
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qdmp-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "qdmp-cli",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"api.js",
|
|
10
10
|
"constants.js",
|
|
11
11
|
"index.js",
|
|
12
|
+
"openapi.js",
|
|
12
13
|
"utils/"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
@@ -40,4 +41,4 @@
|
|
|
40
41
|
"engines": {
|
|
41
42
|
"node": "^20.19.0 || >=22.12.0"
|
|
42
43
|
}
|
|
43
|
-
}
|
|
44
|
+
}
|
package/utils/authorized.js
CHANGED
|
@@ -29,15 +29,16 @@ export const saveToken = (token) => {
|
|
|
29
29
|
* @returns {string} - 认证令牌
|
|
30
30
|
*/
|
|
31
31
|
export const getToken = () => {
|
|
32
|
+
const environmentToken = process.env.QDMP_TOKEN?.trim();
|
|
33
|
+
if (environmentToken) return environmentToken;
|
|
32
34
|
const configPath = getConfigPath();
|
|
33
35
|
try {
|
|
34
36
|
if (fs.existsSync(configPath)) {
|
|
35
37
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
36
38
|
return config.token || null;
|
|
37
|
-
} else {
|
|
38
|
-
error("认证令牌不存在,请重新登录");
|
|
39
39
|
}
|
|
40
40
|
} catch (err) {
|
|
41
|
-
|
|
41
|
+
return null;
|
|
42
42
|
}
|
|
43
|
+
return null;
|
|
43
44
|
};
|
package/utils/common.js
CHANGED
|
@@ -33,6 +33,28 @@ export function getAppId(env = "prod") {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function resolveAppId(cwd = process.cwd(), env = "prod") {
|
|
37
|
+
const fieldNames = env === "prod" ? ["appId"] : ["devAppId", "appId"];
|
|
38
|
+
const configPaths = [
|
|
39
|
+
path.join(cwd, "qdmp-config.json"),
|
|
40
|
+
path.join(cwd, "frontend", "qdmp.json"),
|
|
41
|
+
path.join(cwd, "qdmp.json"),
|
|
42
|
+
];
|
|
43
|
+
for (const configPath of configPaths) {
|
|
44
|
+
if (!fs.existsSync(configPath)) continue;
|
|
45
|
+
try {
|
|
46
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
47
|
+
for (const fieldName of fieldNames) {
|
|
48
|
+
const appId = String(config?.[fieldName] || "").trim();
|
|
49
|
+
if (appId) return appId;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// 继续检查兼容配置,最终由调用方给出统一错误。
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
/** 验证URL是否可访问
|
|
37
59
|
* @param {*} url
|
|
38
60
|
*/
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const HTTP_METHOD_PATTERN = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/i;
|
|
5
|
+
|
|
6
|
+
const normalizePath = (value) => {
|
|
7
|
+
const raw = String(value || "").trim();
|
|
8
|
+
if (!raw) return "";
|
|
9
|
+
try {
|
|
10
|
+
if (/^https?:\/\//i.test(raw)) return new URL(raw).pathname.replace(/\/+$/, "") || "/";
|
|
11
|
+
} catch {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
return (raw.split(/[?#]/, 1)[0].replace(/\/+$/, "") || "/");
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const normalizeDescriptor = (entry, index) => {
|
|
18
|
+
if (typeof entry === "string") {
|
|
19
|
+
const value = entry.trim();
|
|
20
|
+
const separator = value.indexOf(" ");
|
|
21
|
+
if (separator > 0 && HTTP_METHOD_PATTERN.test(value.slice(0, separator))) {
|
|
22
|
+
return {
|
|
23
|
+
method: value.slice(0, separator).toUpperCase(),
|
|
24
|
+
path: normalizePath(value.slice(separator + 1)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (value) return { showCode: value };
|
|
28
|
+
}
|
|
29
|
+
if (entry && typeof entry === "object") {
|
|
30
|
+
const descriptor = {
|
|
31
|
+
method: String(entry.method || "").trim().toUpperCase(),
|
|
32
|
+
path: normalizePath(entry.path || entry.url),
|
|
33
|
+
showCode: String(entry.showCode || "").trim(),
|
|
34
|
+
apiCode: String(entry.apiCode || "").trim(),
|
|
35
|
+
};
|
|
36
|
+
if ((descriptor.method && descriptor.path) || descriptor.showCode || descriptor.apiCode) {
|
|
37
|
+
if (descriptor.method && !HTTP_METHOD_PATTERN.test(descriptor.method)) {
|
|
38
|
+
throw new Error(`openapis[${index}].method 不是有效 HTTP 方法`);
|
|
39
|
+
}
|
|
40
|
+
return descriptor;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`openapis[${index}] 缺少 method + path、showCode 或 apiCode`);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const readOpenAPIManifest = (manifestPath, cwd = process.cwd()) => {
|
|
47
|
+
const absolutePath = path.resolve(cwd, manifestPath);
|
|
48
|
+
if (!fs.existsSync(absolutePath)) {
|
|
49
|
+
throw new Error(`未找到 OpenAPI 使用清单:${absolutePath}`);
|
|
50
|
+
}
|
|
51
|
+
let manifest;
|
|
52
|
+
try {
|
|
53
|
+
manifest = JSON.parse(fs.readFileSync(absolutePath, "utf8"));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(`OpenAPI 使用清单不是有效 JSON:${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
if (manifest?.schemaVersion !== 1 || !Array.isArray(manifest?.openapis)) {
|
|
58
|
+
throw new Error("OpenAPI 使用清单必须包含 schemaVersion: 1 和 openapis 数组");
|
|
59
|
+
}
|
|
60
|
+
if (manifest.openapis.length === 0) {
|
|
61
|
+
throw new Error("OpenAPI 使用清单中的 openapis 不能为空");
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
path: absolutePath,
|
|
65
|
+
openapis: manifest.openapis.map(normalizeDescriptor),
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const sameText = (left, right) => String(left || "").trim() === String(right || "").trim();
|
|
70
|
+
|
|
71
|
+
const resourceMatches = (descriptor, item) => {
|
|
72
|
+
const resource = item?.resource || {};
|
|
73
|
+
if (descriptor.showCode && !sameText(descriptor.showCode, resource.showCode)) return false;
|
|
74
|
+
if (descriptor.apiCode && !sameText(descriptor.apiCode, resource.apiCode)) return false;
|
|
75
|
+
if (descriptor.method && descriptor.method !== String(resource.method || "").trim().toUpperCase()) return false;
|
|
76
|
+
if (descriptor.path && descriptor.path !== normalizePath(resource.path)) return false;
|
|
77
|
+
return true;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const resolveOpenAPIManifest = (descriptors, catalog) => {
|
|
81
|
+
const resolved = [];
|
|
82
|
+
const seenResourceIDs = new Set();
|
|
83
|
+
for (const descriptor of descriptors) {
|
|
84
|
+
const matches = catalog.filter((item) => resourceMatches(descriptor, item));
|
|
85
|
+
if (matches.length === 0) {
|
|
86
|
+
throw new Error(`未找到 OpenAPI:${JSON.stringify(descriptor)}`);
|
|
87
|
+
}
|
|
88
|
+
if (matches.length > 1) {
|
|
89
|
+
throw new Error(`OpenAPI 匹配不唯一,请补充 method + path 或唯一编码:${JSON.stringify(descriptor)}`);
|
|
90
|
+
}
|
|
91
|
+
const item = matches[0];
|
|
92
|
+
const resourceId = String(item?.resource?.resourceId || "");
|
|
93
|
+
if (!resourceId) throw new Error(`OpenAPI 缺少 resourceId:${JSON.stringify(descriptor)}`);
|
|
94
|
+
if (seenResourceIDs.has(resourceId)) continue;
|
|
95
|
+
seenResourceIDs.add(resourceId);
|
|
96
|
+
resolved.push(item);
|
|
97
|
+
}
|
|
98
|
+
return resolved;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const classifyOpenAPIResources = (items) => {
|
|
102
|
+
const result = {
|
|
103
|
+
required: [],
|
|
104
|
+
granted: [],
|
|
105
|
+
pending: [],
|
|
106
|
+
noPermissionRequired: [],
|
|
107
|
+
unavailable: [],
|
|
108
|
+
};
|
|
109
|
+
for (const item of items) {
|
|
110
|
+
const resource = item.resource || {};
|
|
111
|
+
const appContext = item.appContext || {};
|
|
112
|
+
const resourceId = String(resource.resourceId || "");
|
|
113
|
+
const accessStatus = String(appContext.accessStatus || "").toUpperCase();
|
|
114
|
+
const applyStatus = String(appContext.applyStatus || "").toUpperCase();
|
|
115
|
+
if (String(resource.permissionPolicy || "").toUpperCase() === "NO_NEED_PERMISSION") {
|
|
116
|
+
result.noPermissionRequired.push(resourceId);
|
|
117
|
+
} else if (String(appContext.grantStatus || "").toUpperCase() === "GRANTED" || accessStatus === "GRANTED") {
|
|
118
|
+
result.granted.push(resourceId);
|
|
119
|
+
} else if (accessStatus === "PENDING" || applyStatus.includes("PENDING")) {
|
|
120
|
+
result.pending.push(resourceId);
|
|
121
|
+
} else if (appContext.capability?.canApply === true) {
|
|
122
|
+
result.required.push(resourceId);
|
|
123
|
+
} else {
|
|
124
|
+
result.unavailable.push(resourceId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
};
|