digitalsee-ai-flow-cli 0.7.6 → 0.7.24-beta.1
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 +84 -22
- package/dist/api/client.js +34 -4
- package/dist/api/knowledge.js +10 -8
- package/dist/api/node.js +36 -0
- package/dist/api/refresh.js +81 -0
- package/dist/api/service-log.js +125 -0
- package/dist/commands/auth.js +39 -15
- package/dist/commands/config.js +5 -3
- package/dist/commands/flow/helpers.js +78 -2
- package/dist/commands/flow/node.js +103 -17
- package/dist/commands/knowledge.js +38 -16
- package/dist/commands/profile.js +19 -4
- package/dist/commands/service-log.js +286 -0
- package/dist/index.js +2 -0
- package/dist/services/skillGenerator.js +62 -13
- package/dist/utils/common.js +16 -0
- package/dist/utils/config.js +9 -2
- package/dist/utils/format.js +14 -0
- package/dist/utils/nodePackage.js +87 -0
- package/dist/utils/oauth.js +27 -8
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ ai-flow auth status --json
|
|
|
98
98
|
|
|
99
99
|
`--json` 输出示例:
|
|
100
100
|
```json
|
|
101
|
-
{ "ok": true, "loggedIn": true, "activeProfile": "dev", "token": "abcd****1234", "baseUrl": "https://dev.example.com" }
|
|
101
|
+
{ "ok": true, "data": { "loggedIn": true, "activeProfile": "dev", "token": "abcd****1234", "baseUrl": "https://dev.example.com" } }
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
### auth logout
|
|
@@ -188,6 +188,22 @@ ai-flow profile show --name prod # 指定 Profile
|
|
|
188
188
|
ai-flow profile show --json
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
输出示例(当前 Profile):
|
|
192
|
+
```
|
|
193
|
+
==================== Profile: dev ====================
|
|
194
|
+
baseUrl: https://dev.example.com
|
|
195
|
+
token: abcd****1234
|
|
196
|
+
insecure: false
|
|
197
|
+
Node Package: 支持
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`--json` 输出示例:
|
|
201
|
+
```json
|
|
202
|
+
{ "ok": true, "data": { "baseUrl": "https://dev.example.com", "token": "abcd****1234", "insecure": false, "nodePackageSupported": true } }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
> Node Package 状态在登录后自动检测缓存。查看非当前 Profile 时该字段为 `undefined`(不检测)。
|
|
206
|
+
|
|
191
207
|
### profile delete / rename
|
|
192
208
|
|
|
193
209
|
删除或重命名 Profile。
|
|
@@ -1284,6 +1300,57 @@ ai-flow sync status --id <connectorId> --json
|
|
|
1284
1300
|
|
|
1285
1301
|
---
|
|
1286
1302
|
|
|
1303
|
+
## 运维日志
|
|
1304
|
+
|
|
1305
|
+
查询和实时跟踪运维服务日志。
|
|
1306
|
+
|
|
1307
|
+
### service-log search
|
|
1308
|
+
|
|
1309
|
+
查询历史运维日志,按时间范围、级别、关键词等条件筛选。
|
|
1310
|
+
|
|
1311
|
+
```bash
|
|
1312
|
+
ai-flow service-log search --time-range 1h
|
|
1313
|
+
ai-flow service-log search --time-range 3h --level ERROR
|
|
1314
|
+
ai-flow service-log search --time-range 7d --keyword "timeout" --service-type iam
|
|
1315
|
+
ai-flow service-log search --start-time "2026-07-01 00:00:00" --end-time "2026-07-20 23:59:59"
|
|
1316
|
+
ai-flow service-log search --time-range 24h --limit 100 --json
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
| 选项 | 必填 | 默认值 | 说明 |
|
|
1320
|
+
|------|------|--------|------|
|
|
1321
|
+
| `--time-range` | 二选一 | — | 相对时间范围,格式: `<N>s\|m\|h\|d`(如 `30s`, `15m`, `3h`, `7d`),与 `--start-time` 互斥 |
|
|
1322
|
+
| `--start-time` | 二选一 | — | 开始时间 `yyyy-MM-dd HH:mm:ss`,与 `--time-range` 互斥 |
|
|
1323
|
+
| `--end-time` | 否 | 请求时刻 | 结束时间 `yyyy-MM-dd HH:mm:ss` |
|
|
1324
|
+
| `--limit` | 否 | `50` | 最大返回条数(上限 500) |
|
|
1325
|
+
| `--level` | 否 | — | 日志级别:`INFO` / `WARN` / `ERROR` |
|
|
1326
|
+
| `--request-id` | 否 | — | 链路 trace id(精确匹配) |
|
|
1327
|
+
| `--keyword` | 否 | — | 日志正文关键词 |
|
|
1328
|
+
| `--service-type` | 否 | — | 服务类型:`iam` / `acm` / `ncm` / `adm` / `mdm` / `nginx` 等 |
|
|
1329
|
+
| `--json` | 否 | — | 以 JSON 格式输出 `{ ok, data, total }` |
|
|
1330
|
+
|
|
1331
|
+
### service-log stream
|
|
1332
|
+
|
|
1333
|
+
通过 WebSocket 实时跟随运维日志,按 `Ctrl+C` 退出。连接前自动校验 token 可用性。
|
|
1334
|
+
|
|
1335
|
+
```bash
|
|
1336
|
+
ai-flow service-log stream
|
|
1337
|
+
ai-flow service-log stream --level ERROR --service-type iam
|
|
1338
|
+
ai-flow service-log stream --keyword "exception" --max 100
|
|
1339
|
+
ai-flow service-log stream --json # NDJSON 输出(每行一条 { ok, data })
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
| 选项 | 必填 | 默认值 | 说明 |
|
|
1343
|
+
|------|------|--------|------|
|
|
1344
|
+
| `--level` | 否 | — | 日志级别:`INFO` / `WARN` / `ERROR` |
|
|
1345
|
+
| `--request-id` | 否 | — | 链路 trace id(精确匹配) |
|
|
1346
|
+
| `--keyword` | 否 | — | 日志正文关键词 |
|
|
1347
|
+
| `--service-type` | 否 | — | 服务类型:`iam` / `acm` / `ncm` / `adm` / `mdm` / `nginx` 等 |
|
|
1348
|
+
| `--limit` | 否 | `20` | 单次 ES 拉取批量 |
|
|
1349
|
+
| `--max` | 否 | — | 收到 N 条后自动退出(默认不限) |
|
|
1350
|
+
| `--json` | 否 | — | NDJSON 输出,每行一条 `{ ok, data }` |
|
|
1351
|
+
|
|
1352
|
+
---
|
|
1353
|
+
|
|
1287
1354
|
## 知识库
|
|
1288
1355
|
|
|
1289
1356
|
### knowledge categories
|
|
@@ -1292,8 +1359,7 @@ ai-flow sync status --id <connectorId> --json
|
|
|
1292
1359
|
|
|
1293
1360
|
```bash
|
|
1294
1361
|
ai-flow knowledge categories
|
|
1295
|
-
ai-flow knowledge categories --
|
|
1296
|
-
ai-flow knowledge categories --type trigger
|
|
1362
|
+
ai-flow knowledge categories --trigger
|
|
1297
1363
|
ai-flow knowledge categories --json
|
|
1298
1364
|
```
|
|
1299
1365
|
|
|
@@ -1304,7 +1370,7 @@ ai-flow knowledge categories --json
|
|
|
1304
1370
|
```bash
|
|
1305
1371
|
ai-flow knowledge nodes
|
|
1306
1372
|
ai-flow knowledge nodes <category>
|
|
1307
|
-
ai-flow knowledge nodes --name "sql"
|
|
1373
|
+
ai-flow knowledge nodes --name "sql"
|
|
1308
1374
|
ai-flow knowledge nodes --name "HTTP" --trigger --page 1 --size 20
|
|
1309
1375
|
ai-flow knowledge nodes --json
|
|
1310
1376
|
```
|
|
@@ -1344,7 +1410,7 @@ ai-flow knowledge actions <linkId> --json # JSON 格式输出(含 outpu
|
|
|
1344
1410
|
|
|
1345
1411
|
### knowledge generate-skills
|
|
1346
1412
|
|
|
1347
|
-
|
|
1413
|
+
遍历所有分类下的所有节点和动作,为每个动作生成 Markdown 文档和 JSON 结构化数据文件,按分类类型和分类名组织存储。支持 `node_schema` 模式(包含 input/output fields 详情)。
|
|
1348
1414
|
|
|
1349
1415
|
```bash
|
|
1350
1416
|
ai-flow knowledge generate-skills ./docs
|
|
@@ -1356,26 +1422,20 @@ ai-flow knowledge generate-skills ./docs --json
|
|
|
1356
1422
|
输出:
|
|
1357
1423
|
```
|
|
1358
1424
|
docs/
|
|
1359
|
-
├──
|
|
1360
|
-
│ ├──
|
|
1425
|
+
├── normal/
|
|
1426
|
+
│ ├── HTTP/
|
|
1361
1427
|
│ │ ├── HTTP_发送请求.md
|
|
1362
|
-
│ │ └──
|
|
1363
|
-
│ └──
|
|
1364
|
-
│ ├──
|
|
1365
|
-
│ └──
|
|
1366
|
-
├── trigger/
|
|
1367
|
-
│
|
|
1368
|
-
│
|
|
1369
|
-
│
|
|
1370
|
-
│ └── json/
|
|
1371
|
-
│ └── ...
|
|
1372
|
-
├── tool/
|
|
1373
|
-
│ └── ...
|
|
1428
|
+
│ │ └── HTTP_发送请求.json
|
|
1429
|
+
│ └── MySQL/
|
|
1430
|
+
│ ├── MySQL_执行SQL查询.md
|
|
1431
|
+
│ └── MySQL_执行SQL查询.json
|
|
1432
|
+
├── normal-trigger/
|
|
1433
|
+
│ └── Webhook/
|
|
1434
|
+
│ ├── Webhook_接收数据v2.md
|
|
1435
|
+
│ └── Webhook_接收数据v2.json
|
|
1374
1436
|
├── mcp/
|
|
1375
1437
|
│ └── ...
|
|
1376
|
-
|
|
1377
|
-
│ └── ...
|
|
1378
|
-
└── memory/
|
|
1438
|
+
└── mcp-trigger/
|
|
1379
1439
|
└── ...
|
|
1380
1440
|
```
|
|
1381
1441
|
|
|
@@ -1563,6 +1623,7 @@ src/
|
|
|
1563
1623
|
│ │ ├── cancel-sync.ts
|
|
1564
1624
|
│ │ └── status.ts
|
|
1565
1625
|
│ ├── org.ts # ai-flow org(组织架构)
|
|
1626
|
+
│ ├── service-log.ts # ai-flow service-log(运维日志)
|
|
1566
1627
|
│ └── knowledge.ts # ai-flow knowledge
|
|
1567
1628
|
├── api/
|
|
1568
1629
|
│ ├── client.ts # axios 实例(SSL、Token)
|
|
@@ -1574,6 +1635,7 @@ src/
|
|
|
1574
1635
|
│ ├── link.ts # 上游连接器 API
|
|
1575
1636
|
│ ├── mapping.ts # 属性映射 API
|
|
1576
1637
|
│ ├── sync.ts # 下游连接器 API
|
|
1638
|
+
│ ├── service-log.ts # 运维日志 API(search + stream)
|
|
1577
1639
|
│ └── org.ts # 组织架构 API
|
|
1578
1640
|
├── services/
|
|
1579
1641
|
│ ├── flowAnalyzer.ts # DAG 拓扑分析
|
package/dist/api/client.js
CHANGED
|
@@ -14,6 +14,7 @@ exports.apiDelete = apiDelete;
|
|
|
14
14
|
const https_1 = __importDefault(require("https"));
|
|
15
15
|
const axios_1 = __importDefault(require("axios"));
|
|
16
16
|
const config_1 = require("@/utils/config");
|
|
17
|
+
const refresh_1 = require("@/api/refresh");
|
|
17
18
|
let clientInstance = null;
|
|
18
19
|
function classifyError(error) {
|
|
19
20
|
if (error && typeof error === 'object') {
|
|
@@ -116,10 +117,19 @@ function buildClient() {
|
|
|
116
117
|
},
|
|
117
118
|
timeout: 30000,
|
|
118
119
|
});
|
|
119
|
-
instance.interceptors.request.use((reqConfig) => {
|
|
120
|
+
instance.interceptors.request.use(async (reqConfig) => {
|
|
120
121
|
const cfg = (0, config_1.loadConfig)();
|
|
121
|
-
if (cfg.
|
|
122
|
-
|
|
122
|
+
if (cfg.auth?.access_token) {
|
|
123
|
+
// 临近过期时主动刷新,避免发送已知过期的 token
|
|
124
|
+
if ((0, refresh_1.isTokenExpired)() && cfg.auth.refresh_token) {
|
|
125
|
+
const newAuth = await (0, refresh_1.attemptTokenRefresh)();
|
|
126
|
+
if (newAuth) {
|
|
127
|
+
reqConfig.headers.Authorization = `${newAuth.token_type ?? 'Bearer'} ${newAuth.access_token}`;
|
|
128
|
+
return reqConfig;
|
|
129
|
+
}
|
|
130
|
+
// 刷新失败不阻塞,继续用当前 token,401 interceptor 会再尝试
|
|
131
|
+
}
|
|
132
|
+
reqConfig.headers.Authorization = `${cfg.auth.token_type ?? 'Bearer'} ${cfg.auth.access_token}`;
|
|
123
133
|
}
|
|
124
134
|
return reqConfig;
|
|
125
135
|
});
|
|
@@ -127,9 +137,29 @@ function buildClient() {
|
|
|
127
137
|
const body = response.data;
|
|
128
138
|
if (body && typeof body === 'object' && 'data' in body) {
|
|
129
139
|
response.data = body.data;
|
|
140
|
+
//钉钉网关可能会触发 code: -1, Access Denied
|
|
141
|
+
if (body.data === null && body.code === '-1') {
|
|
142
|
+
throw new Error(humanizeError(body.message ?? body));
|
|
143
|
+
}
|
|
130
144
|
}
|
|
131
145
|
return response;
|
|
132
|
-
}, (error) => {
|
|
146
|
+
}, async (error) => {
|
|
147
|
+
const errObj = error;
|
|
148
|
+
const originalRequest = errObj.config;
|
|
149
|
+
if (errObj.response?.status === 401 &&
|
|
150
|
+
originalRequest &&
|
|
151
|
+
!originalRequest._retry) {
|
|
152
|
+
const cfg = (0, config_1.loadConfig)();
|
|
153
|
+
if (cfg.auth?.refresh_token) {
|
|
154
|
+
originalRequest._retry = true;
|
|
155
|
+
const newAuth = await (0, refresh_1.attemptTokenRefresh)();
|
|
156
|
+
if (newAuth) {
|
|
157
|
+
originalRequest.headers = originalRequest.headers || {};
|
|
158
|
+
originalRequest.headers.Authorization = `${newAuth.token_type ?? 'Bearer'} ${newAuth.access_token}`;
|
|
159
|
+
return instance.request(originalRequest);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
133
163
|
throw new Error(humanizeError(error));
|
|
134
164
|
});
|
|
135
165
|
return instance;
|
package/dist/api/knowledge.js
CHANGED
|
@@ -6,21 +6,23 @@ exports.getConnectorDetail = getConnectorDetail;
|
|
|
6
6
|
exports.getAllConnectors = getAllConnectors;
|
|
7
7
|
const client_1 = require("@/api/client");
|
|
8
8
|
async function getNodeCategories(params) {
|
|
9
|
-
return (0, client_1.
|
|
9
|
+
return (0, client_1.apiPost)('/acm/node-package/categories/connectable', {
|
|
10
|
+
scene: params?.scene || 'normal',
|
|
11
|
+
trigger: params?.trigger || false,
|
|
12
|
+
});
|
|
10
13
|
}
|
|
11
14
|
async function searchNodesByCategory(params) {
|
|
12
15
|
const body = {
|
|
16
|
+
scene: 'normal',
|
|
13
17
|
page: params.page || 1,
|
|
14
18
|
size: params.size || 50,
|
|
15
19
|
trigger: params.trigger,
|
|
16
20
|
};
|
|
17
|
-
if (params.
|
|
18
|
-
body.
|
|
19
|
-
if (params.
|
|
20
|
-
body.
|
|
21
|
-
|
|
22
|
-
body.app_domain_category = params.app_domain_category;
|
|
23
|
-
const data = await (0, client_1.apiPost)('/acm/links/search', body);
|
|
21
|
+
if (params.keyword)
|
|
22
|
+
body.keyword = params.keyword;
|
|
23
|
+
if (params.category_name)
|
|
24
|
+
body.category_name = params.category_name;
|
|
25
|
+
const data = await (0, client_1.apiPost)('/acm/node-package/plugins/connectable', body);
|
|
24
26
|
return data || { items: [], total: 0, page: params.page || 1, size: params.size || 50 };
|
|
25
27
|
}
|
|
26
28
|
async function getConnectorDetail(connectorId) {
|
package/dist/api/node.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getActions = getActions;
|
|
4
|
+
exports.searchActions = searchActions;
|
|
5
|
+
exports.isNodePackageSupported = isNodePackageSupported;
|
|
4
6
|
exports.getNodeDetail = getNodeDetail;
|
|
5
7
|
exports.createNode = createNode;
|
|
6
8
|
exports.updateNode = updateNode;
|
|
@@ -9,6 +11,7 @@ exports.deleteNode = deleteNode;
|
|
|
9
11
|
exports.testRunNode = testRunNode;
|
|
10
12
|
exports.cancelDebugNode = cancelDebugNode;
|
|
11
13
|
const client_1 = require("@/api/client");
|
|
14
|
+
const knowledge_1 = require("@/api/knowledge");
|
|
12
15
|
const encoding_1 = require("@/utils/encoding");
|
|
13
16
|
async function getActions(params) {
|
|
14
17
|
const data = await (0, client_1.apiGet)('/acm/actions/v2', params);
|
|
@@ -22,6 +25,39 @@ async function getActions(params) {
|
|
|
22
25
|
return data.data;
|
|
23
26
|
return [];
|
|
24
27
|
}
|
|
28
|
+
async function searchActions(params) {
|
|
29
|
+
const data = await (0, client_1.apiPost)('/acm/links/search', params);
|
|
30
|
+
const actions = [];
|
|
31
|
+
if (data && data.items && Array.isArray(data.items)) {
|
|
32
|
+
for (const item of data.items) {
|
|
33
|
+
if (item.matched_actions && Array.isArray(item.matched_actions)) {
|
|
34
|
+
actions.push(...item.matched_actions);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return actions;
|
|
39
|
+
}
|
|
40
|
+
async function isNodePackageSupported() {
|
|
41
|
+
try {
|
|
42
|
+
const categories = await (0, knowledge_1.getNodeCategories)();
|
|
43
|
+
const firstName = categories[0]?.name;
|
|
44
|
+
if (!firstName)
|
|
45
|
+
return false;
|
|
46
|
+
const { items } = await (0, knowledge_1.searchNodesByCategory)({
|
|
47
|
+
category_name: firstName,
|
|
48
|
+
trigger: true,
|
|
49
|
+
size: 1,
|
|
50
|
+
});
|
|
51
|
+
const linkId = items?.[0]?.connector?.id;
|
|
52
|
+
if (!linkId)
|
|
53
|
+
return false;
|
|
54
|
+
const actions = await getActions({ linkId });
|
|
55
|
+
return actions.some((action) => action.hasOwnProperty('node_schema'));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
25
61
|
async function getNodeDetail(nodeId) {
|
|
26
62
|
return (0, client_1.apiGet)(`/acm/flows/nodes/${nodeId}`);
|
|
27
63
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.attemptTokenRefresh = attemptTokenRefresh;
|
|
7
|
+
exports.isTokenExpired = isTokenExpired;
|
|
8
|
+
const https_1 = __importDefault(require("https"));
|
|
9
|
+
const axios_1 = __importDefault(require("axios"));
|
|
10
|
+
const config_1 = require("@/utils/config");
|
|
11
|
+
/**
|
|
12
|
+
* 并发控制:多个 401 同时到达时,只发一次 refresh 请求,
|
|
13
|
+
* 其他调用复用同一个 Promise。
|
|
14
|
+
*/
|
|
15
|
+
let isRefreshing = false;
|
|
16
|
+
let pendingRefresh = null;
|
|
17
|
+
const EXPIRY_BUFFER_MS = 120000; // 提前 120 秒刷新,避免临界区过期
|
|
18
|
+
/**
|
|
19
|
+
* 使用 refresh_token 向 /iam/token 获取新 access_token,
|
|
20
|
+
* 成功后自动写入 global config 并返回新的 AuthConfig。
|
|
21
|
+
* 无 refresh_token 或刷新失败返回 null。
|
|
22
|
+
*/
|
|
23
|
+
async function attemptTokenRefresh() {
|
|
24
|
+
const cfg = (0, config_1.loadConfig)();
|
|
25
|
+
if (!cfg.auth?.refresh_token) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// 去重:如果已有正在进行的刷新,复用同一个 Promise
|
|
29
|
+
if (isRefreshing && pendingRefresh) {
|
|
30
|
+
return pendingRefresh;
|
|
31
|
+
}
|
|
32
|
+
isRefreshing = true;
|
|
33
|
+
pendingRefresh = (async () => {
|
|
34
|
+
try {
|
|
35
|
+
const httpsAgent = new https_1.default.Agent({ rejectUnauthorized: !cfg.insecure });
|
|
36
|
+
const res = await axios_1.default.post(`${cfg.baseUrl}/iam/token`, {
|
|
37
|
+
grant_type: 'refresh_token',
|
|
38
|
+
client_id: 'usercenter',
|
|
39
|
+
refresh_token: cfg.auth.refresh_token,
|
|
40
|
+
}, {
|
|
41
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
42
|
+
httpsAgent,
|
|
43
|
+
timeout: 15000,
|
|
44
|
+
// 不使用 getClient(),避免触发 request/response 拦截器
|
|
45
|
+
});
|
|
46
|
+
const { access_token, refresh_token: newRt, token_type, expires_in } = res.data;
|
|
47
|
+
const newAuth = {
|
|
48
|
+
access_token,
|
|
49
|
+
refresh_token: newRt || cfg.auth.refresh_token,
|
|
50
|
+
token_type: token_type || cfg.auth.token_type || 'Bearer',
|
|
51
|
+
// 使用客户端本地时间计算过期时刻,服务端不支持返回绝对过期时间
|
|
52
|
+
expires_at: Date.now() + (expires_in || 3600) * 1000,
|
|
53
|
+
};
|
|
54
|
+
(0, config_1.saveGlobalConfig)({ auth: newAuth });
|
|
55
|
+
return newAuth;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const detail = err instanceof Error ? err.message : JSON.stringify(err);
|
|
59
|
+
process.stderr.write(`[refresh] token 刷新失败: ${detail}\n`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
isRefreshing = false;
|
|
64
|
+
pendingRefresh = null;
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
return pendingRefresh;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 基于 config.auth.expires_at 判断 token 是否已(或即将)过期。
|
|
71
|
+
* 包含 120 秒缓冲,避免 token 在飞行期间过期。
|
|
72
|
+
* 无 expires_at 时默认返回 false(不阻塞,让 401 响应来驱动刷新)。
|
|
73
|
+
*/
|
|
74
|
+
function isTokenExpired() {
|
|
75
|
+
const cfg = (0, config_1.loadConfig)();
|
|
76
|
+
if (!cfg.auth?.expires_at) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return Date.now() >= cfg.auth.expires_at - EXPIRY_BUFFER_MS;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=refresh.js.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.verifyToken = verifyToken;
|
|
7
|
+
exports.searchServiceLogs = searchServiceLogs;
|
|
8
|
+
exports.openServiceLogStream = openServiceLogStream;
|
|
9
|
+
const https_1 = __importDefault(require("https"));
|
|
10
|
+
const axios_1 = __importDefault(require("axios"));
|
|
11
|
+
const ws_1 = __importDefault(require("ws"));
|
|
12
|
+
const config_1 = require("@/utils/config");
|
|
13
|
+
const client_1 = require("@/api/client");
|
|
14
|
+
const SEARCH_PATH = '/iam/api/service-log/search';
|
|
15
|
+
const STREAM_PATH = '/iam/api/service-log/stream';
|
|
16
|
+
/**
|
|
17
|
+
* 验证当前 token 是否可用(发起轻量 GET 请求)
|
|
18
|
+
* token 无效时抛出 axios interceptor 格式化的错误
|
|
19
|
+
*/
|
|
20
|
+
async function verifyToken() {
|
|
21
|
+
const client = (0, client_1.getClient)();
|
|
22
|
+
await client.get('/iam/api/connectors');
|
|
23
|
+
}
|
|
24
|
+
function buildHttpClient() {
|
|
25
|
+
const cfg = (0, config_1.loadConfig)();
|
|
26
|
+
return axios_1.default.create({
|
|
27
|
+
baseURL: cfg.baseUrl,
|
|
28
|
+
httpsAgent: new https_1.default.Agent({ rejectUnauthorized: !cfg.insecure }),
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
Authorization: `${cfg.auth?.token_type ?? 'Bearer'} ${cfg.auth?.access_token}`,
|
|
32
|
+
},
|
|
33
|
+
timeout: 30000,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 本地校验 envelope:现有 client.ts 拦截器只解包 data、不检查 result 字段,
|
|
38
|
+
* 而文档 2.6 规定业务错误(如 "limit 不能超过 500")以 HTTP 200 + result=false 返回,
|
|
39
|
+
* 因此本函数不复用 apiPost,独立 POST 并自行检查 envelope。
|
|
40
|
+
*/
|
|
41
|
+
async function searchServiceLogs(req) {
|
|
42
|
+
const client = buildHttpClient();
|
|
43
|
+
const response = await client.post(SEARCH_PATH, req);
|
|
44
|
+
const body = response.data;
|
|
45
|
+
if (Array.isArray(body)) {
|
|
46
|
+
return body;
|
|
47
|
+
}
|
|
48
|
+
if (body && typeof body === 'object' && 'result' in body) {
|
|
49
|
+
if (body.result === false) {
|
|
50
|
+
throw new Error(body.error_description || body.error || '日志查询失败');
|
|
51
|
+
}
|
|
52
|
+
return body.data ?? [];
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
function toWsUrl(baseUrl) {
|
|
57
|
+
const trimmed = baseUrl.replace(/\/+$/, '');
|
|
58
|
+
if (trimmed.startsWith('https://')) {
|
|
59
|
+
return `wss://${trimmed.slice(8)}${STREAM_PATH}`;
|
|
60
|
+
}
|
|
61
|
+
if (trimmed.startsWith('http://')) {
|
|
62
|
+
return `ws://${trimmed.slice(7)}${STREAM_PATH}`;
|
|
63
|
+
}
|
|
64
|
+
return `${trimmed}${STREAM_PATH}`;
|
|
65
|
+
}
|
|
66
|
+
function openServiceLogStream(filters, handlers) {
|
|
67
|
+
const cfg = (0, config_1.loadConfig)();
|
|
68
|
+
if (!cfg.auth?.access_token) {
|
|
69
|
+
throw new Error('未登录,请先执行 ai-flow auth login');
|
|
70
|
+
}
|
|
71
|
+
const url = toWsUrl(cfg.baseUrl);
|
|
72
|
+
const ws = new ws_1.default(url, {
|
|
73
|
+
headers: { Authorization: `${cfg.auth.token_type ?? 'Bearer'} ${cfg.auth.access_token}` },
|
|
74
|
+
rejectUnauthorized: !cfg.insecure,
|
|
75
|
+
});
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
ws.on('open', () => {
|
|
78
|
+
const subscribeFrame = {
|
|
79
|
+
action: 'subscribe',
|
|
80
|
+
...filters,
|
|
81
|
+
};
|
|
82
|
+
ws.send(JSON.stringify(subscribeFrame));
|
|
83
|
+
handlers.onOpen?.();
|
|
84
|
+
});
|
|
85
|
+
ws.on('message', (raw) => {
|
|
86
|
+
let msg;
|
|
87
|
+
try {
|
|
88
|
+
msg = JSON.parse(raw.toString());
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (msg.type === 'log' && msg.data) {
|
|
94
|
+
handlers.onLog(msg.data);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (msg.type === 'error' && msg.message) {
|
|
98
|
+
handlers.onError(msg.message);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
ws.on('close', (code, reasonBuf) => {
|
|
102
|
+
const reason = reasonBuf?.toString?.() ?? '';
|
|
103
|
+
handlers.onClose(code, reason);
|
|
104
|
+
});
|
|
105
|
+
ws.on('error', (err) => {
|
|
106
|
+
handlers.onError(err.message || 'WebSocket 连接错误');
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
cancel() {
|
|
110
|
+
if (cancelled)
|
|
111
|
+
return;
|
|
112
|
+
cancelled = true;
|
|
113
|
+
if (ws.readyState === ws_1.default.OPEN) {
|
|
114
|
+
ws.send(JSON.stringify({ action: 'cancel' }));
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
ws.close();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// ignore
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=service-log.js.map
|
package/dist/commands/auth.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.registerAuthCommand = registerAuthCommand;
|
|
7
7
|
const config_1 = require("@/utils/config");
|
|
8
|
+
const nodePackage_1 = require("@/utils/nodePackage");
|
|
8
9
|
const oauth_1 = require("@/utils/oauth");
|
|
9
10
|
const output_1 = require("@/utils/output");
|
|
10
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -29,14 +30,14 @@ function registerAuthCommand(program) {
|
|
|
29
30
|
(0, output_1.printInfo)(`使用 Profile: ${(0, config_1.getActiveProfile)()}`);
|
|
30
31
|
const protocol = options.https ? 'https' : 'http';
|
|
31
32
|
(0, output_1.printInfo)(`正在准备 OAuth 登录 (${protocol.toUpperCase()})...`);
|
|
32
|
-
const { server, port,
|
|
33
|
+
const { server, port, waitForResult } = options.https
|
|
33
34
|
? await (0, oauth_1.createAuthServerHttps)()
|
|
34
35
|
: await (0, oauth_1.createAuthServer)();
|
|
35
36
|
const authorizeUrl = (0, oauth_1.buildAuthorizeUrl)(config.baseUrl, port, protocol);
|
|
36
37
|
(0, output_1.printHeader)('OAuth 2 登录');
|
|
37
38
|
(0, output_1.printInfo)('打开下方链接,授权登录');
|
|
38
39
|
console.log(`\n${chalk_1.default.cyan(authorizeUrl)}\n`);
|
|
39
|
-
const
|
|
40
|
+
const resultPromise = waitForResult();
|
|
40
41
|
(0, output_1.printInfo)('等待授权回调...');
|
|
41
42
|
rl = readline.createInterface({ input, output });
|
|
42
43
|
rl.question('按 Enter 在浏览器中打开...\n', () => {
|
|
@@ -50,12 +51,21 @@ function registerAuthCommand(program) {
|
|
|
50
51
|
rl.close();
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
|
-
const
|
|
54
|
+
const { access_token, refresh_token, id_token, token_type, expires_in } = await resultPromise;
|
|
55
|
+
// 使用客户端本地时间计算过期时刻,服务端不支持返回绝对过期时间
|
|
56
|
+
const expires_at = new Date().getTime() + expires_in * 1000;
|
|
54
57
|
const saveData = {
|
|
55
|
-
|
|
58
|
+
auth: {
|
|
59
|
+
access_token,
|
|
60
|
+
refresh_token,
|
|
61
|
+
id_token,
|
|
62
|
+
token_type,
|
|
63
|
+
expires_at,
|
|
64
|
+
},
|
|
56
65
|
};
|
|
57
66
|
(0, config_1.saveGlobalConfig)(saveData);
|
|
58
67
|
(0, output_1.printSuccess)('登录成功');
|
|
68
|
+
(0, nodePackage_1.checkAndCacheNodePackageSupport)().catch(() => { });
|
|
59
69
|
server.close();
|
|
60
70
|
}
|
|
61
71
|
catch (error) {
|
|
@@ -75,11 +85,20 @@ function registerAuthCommand(program) {
|
|
|
75
85
|
if (options.json)
|
|
76
86
|
(0, output_1.setJsonMode)(true);
|
|
77
87
|
const scope = options.project ? 'project' : 'global';
|
|
88
|
+
const defaultConfig = {
|
|
89
|
+
auth: {
|
|
90
|
+
access_token: '',
|
|
91
|
+
refresh_token: '',
|
|
92
|
+
id_token: '',
|
|
93
|
+
token_type: '',
|
|
94
|
+
expires_at: 0,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
78
97
|
if (options.project) {
|
|
79
|
-
(0, config_1.saveProjectConfig)(
|
|
98
|
+
(0, config_1.saveProjectConfig)(defaultConfig);
|
|
80
99
|
}
|
|
81
100
|
else {
|
|
82
|
-
(0, config_1.saveGlobalConfig)(
|
|
101
|
+
(0, config_1.saveGlobalConfig)(defaultConfig);
|
|
83
102
|
}
|
|
84
103
|
if (options.json) {
|
|
85
104
|
(0, output_1.printStructuredResult)({ ok: true, scope, activeProfile: (0, config_1.getActiveProfile)() });
|
|
@@ -105,9 +124,12 @@ function registerAuthCommand(program) {
|
|
|
105
124
|
if (options.json)
|
|
106
125
|
(0, output_1.setJsonMode)(true);
|
|
107
126
|
const config = (0, config_1.loadConfig)();
|
|
108
|
-
if (!config.
|
|
127
|
+
if (!config.auth || !config.auth.access_token) {
|
|
109
128
|
if (options.json) {
|
|
110
|
-
(0, output_1.printStructuredResult)({
|
|
129
|
+
(0, output_1.printStructuredResult)({
|
|
130
|
+
ok: true,
|
|
131
|
+
data: { loggedIn: false, activeProfile: (0, config_1.getActiveProfile)() },
|
|
132
|
+
});
|
|
111
133
|
return;
|
|
112
134
|
}
|
|
113
135
|
(0, output_1.printHeader)('认证状态');
|
|
@@ -116,18 +138,20 @@ function registerAuthCommand(program) {
|
|
|
116
138
|
(0, output_1.printInfo)('请执行 ai-flow auth login 登录');
|
|
117
139
|
return;
|
|
118
140
|
}
|
|
119
|
-
const maskedToken = config.
|
|
120
|
-
? config.
|
|
141
|
+
const maskedToken = config.auth.access_token.length > 8
|
|
142
|
+
? config.auth.access_token.substring(0, 4) +
|
|
121
143
|
'****' +
|
|
122
|
-
config.
|
|
144
|
+
config.auth.access_token.substring(config.auth.access_token.length - 4)
|
|
123
145
|
: '****';
|
|
124
146
|
if (options.json) {
|
|
125
147
|
(0, output_1.printStructuredResult)({
|
|
126
148
|
ok: true,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
data: {
|
|
150
|
+
loggedIn: true,
|
|
151
|
+
activeProfile: (0, config_1.getActiveProfile)(),
|
|
152
|
+
token: maskedToken,
|
|
153
|
+
baseUrl: config.baseUrl,
|
|
154
|
+
},
|
|
131
155
|
});
|
|
132
156
|
return;
|
|
133
157
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -15,7 +15,7 @@ function registerConfigCommand(program) {
|
|
|
15
15
|
(0, output_1.printKeyValue)([
|
|
16
16
|
['Profile', (0, config_1.getActiveProfile)()],
|
|
17
17
|
['baseUrl', config.baseUrl],
|
|
18
|
-
['token', (0, output_1.truncateToken)(
|
|
18
|
+
['token', (0, output_1.truncateToken)((0, config_1.getAccessToken)(config)) || '(未设置)'],
|
|
19
19
|
['insecure', String(config.insecure)],
|
|
20
20
|
]);
|
|
21
21
|
});
|
|
@@ -31,8 +31,10 @@ function registerConfigCommand(program) {
|
|
|
31
31
|
const updates = {};
|
|
32
32
|
if (options.baseUrl)
|
|
33
33
|
updates.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
34
|
-
if (options.token)
|
|
35
|
-
|
|
34
|
+
if (options.token) {
|
|
35
|
+
const currentConfig = (0, config_1.loadConfig)(options.profile || undefined);
|
|
36
|
+
updates.auth = { ...(currentConfig.auth || {}), access_token: options.token };
|
|
37
|
+
}
|
|
36
38
|
if (options.insecure !== undefined)
|
|
37
39
|
updates.insecure = true;
|
|
38
40
|
if (Object.keys(updates).length === 0) {
|