digitalsee-ai-flow-cli 0.7.6 → 0.7.23-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 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
@@ -1284,6 +1284,57 @@ ai-flow sync status --id <connectorId> --json
1284
1284
 
1285
1285
  ---
1286
1286
 
1287
+ ## 运维日志
1288
+
1289
+ 查询和实时跟踪运维服务日志。
1290
+
1291
+ ### service-log search
1292
+
1293
+ 查询历史运维日志,按时间范围、级别、关键词等条件筛选。
1294
+
1295
+ ```bash
1296
+ ai-flow service-log search --time-range 1h
1297
+ ai-flow service-log search --time-range 3h --level ERROR
1298
+ ai-flow service-log search --time-range 7d --keyword "timeout" --service-type iam
1299
+ ai-flow service-log search --start-time "2026-07-01 00:00:00" --end-time "2026-07-20 23:59:59"
1300
+ ai-flow service-log search --time-range 24h --limit 100 --json
1301
+ ```
1302
+
1303
+ | 选项 | 必填 | 默认值 | 说明 |
1304
+ |------|------|--------|------|
1305
+ | `--time-range` | 二选一 | — | 相对时间范围,格式: `<N>s\|m\|h\|d`(如 `30s`, `15m`, `3h`, `7d`),与 `--start-time` 互斥 |
1306
+ | `--start-time` | 二选一 | — | 开始时间 `yyyy-MM-dd HH:mm:ss`,与 `--time-range` 互斥 |
1307
+ | `--end-time` | 否 | 请求时刻 | 结束时间 `yyyy-MM-dd HH:mm:ss` |
1308
+ | `--limit` | 否 | `50` | 最大返回条数(上限 500) |
1309
+ | `--level` | 否 | — | 日志级别:`INFO` / `WARN` / `ERROR` |
1310
+ | `--request-id` | 否 | — | 链路 trace id(精确匹配) |
1311
+ | `--keyword` | 否 | — | 日志正文关键词 |
1312
+ | `--service-type` | 否 | — | 服务类型:`iam` / `acm` / `ncm` / `adm` / `mdm` / `nginx` 等 |
1313
+ | `--json` | 否 | — | 以 JSON 格式输出 `{ ok, data, total }` |
1314
+
1315
+ ### service-log stream
1316
+
1317
+ 通过 WebSocket 实时跟随运维日志,按 `Ctrl+C` 退出。连接前自动校验 token 可用性。
1318
+
1319
+ ```bash
1320
+ ai-flow service-log stream
1321
+ ai-flow service-log stream --level ERROR --service-type iam
1322
+ ai-flow service-log stream --keyword "exception" --max 100
1323
+ ai-flow service-log stream --json # NDJSON 输出(每行一条 { ok, data })
1324
+ ```
1325
+
1326
+ | 选项 | 必填 | 默认值 | 说明 |
1327
+ |------|------|--------|------|
1328
+ | `--level` | 否 | — | 日志级别:`INFO` / `WARN` / `ERROR` |
1329
+ | `--request-id` | 否 | — | 链路 trace id(精确匹配) |
1330
+ | `--keyword` | 否 | — | 日志正文关键词 |
1331
+ | `--service-type` | 否 | — | 服务类型:`iam` / `acm` / `ncm` / `adm` / `mdm` / `nginx` 等 |
1332
+ | `--limit` | 否 | `20` | 单次 ES 拉取批量 |
1333
+ | `--max` | 否 | — | 收到 N 条后自动退出(默认不限) |
1334
+ | `--json` | 否 | — | NDJSON 输出,每行一条 `{ ok, data }` |
1335
+
1336
+ ---
1337
+
1287
1338
  ## 知识库
1288
1339
 
1289
1340
  ### knowledge categories
@@ -1563,6 +1614,7 @@ src/
1563
1614
  │ │ ├── cancel-sync.ts
1564
1615
  │ │ └── status.ts
1565
1616
  │ ├── org.ts # ai-flow org(组织架构)
1617
+ │ ├── service-log.ts # ai-flow service-log(运维日志)
1566
1618
  │ └── knowledge.ts # ai-flow knowledge
1567
1619
  ├── api/
1568
1620
  │ ├── client.ts # axios 实例(SSL、Token)
@@ -1574,6 +1626,7 @@ src/
1574
1626
  │ ├── link.ts # 上游连接器 API
1575
1627
  │ ├── mapping.ts # 属性映射 API
1576
1628
  │ ├── sync.ts # 下游连接器 API
1629
+ │ ├── service-log.ts # 运维日志 API(search + stream)
1577
1630
  │ └── org.ts # 组织架构 API
1578
1631
  ├── services/
1579
1632
  │ ├── flowAnalyzer.ts # DAG 拓扑分析
@@ -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.token) {
122
- reqConfig.headers.Authorization = `Bearer ${cfg.token}`;
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;
@@ -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
@@ -29,14 +29,14 @@ function registerAuthCommand(program) {
29
29
  (0, output_1.printInfo)(`使用 Profile: ${(0, config_1.getActiveProfile)()}`);
30
30
  const protocol = options.https ? 'https' : 'http';
31
31
  (0, output_1.printInfo)(`正在准备 OAuth 登录 (${protocol.toUpperCase()})...`);
32
- const { server, port, waitForCode } = options.https
32
+ const { server, port, waitForResult } = options.https
33
33
  ? await (0, oauth_1.createAuthServerHttps)()
34
34
  : await (0, oauth_1.createAuthServer)();
35
35
  const authorizeUrl = (0, oauth_1.buildAuthorizeUrl)(config.baseUrl, port, protocol);
36
36
  (0, output_1.printHeader)('OAuth 2 登录');
37
37
  (0, output_1.printInfo)('打开下方链接,授权登录');
38
38
  console.log(`\n${chalk_1.default.cyan(authorizeUrl)}\n`);
39
- const codePromise = waitForCode();
39
+ const resultPromise = waitForResult();
40
40
  (0, output_1.printInfo)('等待授权回调...');
41
41
  rl = readline.createInterface({ input, output });
42
42
  rl.question('按 Enter 在浏览器中打开...\n', () => {
@@ -50,9 +50,17 @@ function registerAuthCommand(program) {
50
50
  rl.close();
51
51
  }
52
52
  });
53
- const token = await codePromise;
53
+ const { access_token, refresh_token, id_token, token_type, expires_in } = await resultPromise;
54
+ // 使用客户端本地时间计算过期时刻,服务端不支持返回绝对过期时间
55
+ const expires_at = new Date().getTime() + expires_in * 1000;
54
56
  const saveData = {
55
- token,
57
+ auth: {
58
+ access_token,
59
+ refresh_token,
60
+ id_token,
61
+ token_type,
62
+ expires_at,
63
+ },
56
64
  };
57
65
  (0, config_1.saveGlobalConfig)(saveData);
58
66
  (0, output_1.printSuccess)('登录成功');
@@ -75,11 +83,20 @@ function registerAuthCommand(program) {
75
83
  if (options.json)
76
84
  (0, output_1.setJsonMode)(true);
77
85
  const scope = options.project ? 'project' : 'global';
86
+ const defaultConfig = {
87
+ auth: {
88
+ access_token: '',
89
+ refresh_token: '',
90
+ id_token: '',
91
+ token_type: '',
92
+ expires_at: 0,
93
+ },
94
+ };
78
95
  if (options.project) {
79
- (0, config_1.saveProjectConfig)({ token: '' });
96
+ (0, config_1.saveProjectConfig)(defaultConfig);
80
97
  }
81
98
  else {
82
- (0, config_1.saveGlobalConfig)({ token: '' });
99
+ (0, config_1.saveGlobalConfig)(defaultConfig);
83
100
  }
84
101
  if (options.json) {
85
102
  (0, output_1.printStructuredResult)({ ok: true, scope, activeProfile: (0, config_1.getActiveProfile)() });
@@ -105,9 +122,12 @@ function registerAuthCommand(program) {
105
122
  if (options.json)
106
123
  (0, output_1.setJsonMode)(true);
107
124
  const config = (0, config_1.loadConfig)();
108
- if (!config.token) {
125
+ if (!config.auth || !config.auth.access_token) {
109
126
  if (options.json) {
110
- (0, output_1.printStructuredResult)({ ok: true, loggedIn: false, activeProfile: (0, config_1.getActiveProfile)() });
127
+ (0, output_1.printStructuredResult)({
128
+ ok: true,
129
+ data: { loggedIn: false, activeProfile: (0, config_1.getActiveProfile)() },
130
+ });
111
131
  return;
112
132
  }
113
133
  (0, output_1.printHeader)('认证状态');
@@ -116,18 +136,20 @@ function registerAuthCommand(program) {
116
136
  (0, output_1.printInfo)('请执行 ai-flow auth login 登录');
117
137
  return;
118
138
  }
119
- const maskedToken = config.token.length > 8
120
- ? config.token.substring(0, 4) +
139
+ const maskedToken = config.auth.access_token.length > 8
140
+ ? config.auth.access_token.substring(0, 4) +
121
141
  '****' +
122
- config.token.substring(config.token.length - 4)
142
+ config.auth.access_token.substring(config.auth.access_token.length - 4)
123
143
  : '****';
124
144
  if (options.json) {
125
145
  (0, output_1.printStructuredResult)({
126
146
  ok: true,
127
- loggedIn: true,
128
- activeProfile: (0, config_1.getActiveProfile)(),
129
- token: maskedToken,
130
- baseUrl: config.baseUrl,
147
+ data: {
148
+ loggedIn: true,
149
+ activeProfile: (0, config_1.getActiveProfile)(),
150
+ token: maskedToken,
151
+ baseUrl: config.baseUrl,
152
+ },
131
153
  });
132
154
  return;
133
155
  }
@@ -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)(config.token) || '(未设置)'],
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
- updates.token = options.token;
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) {
@@ -82,7 +82,7 @@ function registerProfileCommand(program) {
82
82
  ok: true,
83
83
  profile: profileName,
84
84
  baseUrl: config.baseUrl,
85
- token: config.token ? (0, output_1.truncateToken)(config.token) : '',
85
+ token: (0, output_1.truncateToken)((0, config_1.getAccessToken)(config)),
86
86
  insecure: config.insecure,
87
87
  });
88
88
  return;
@@ -90,7 +90,7 @@ function registerProfileCommand(program) {
90
90
  (0, output_1.printHeader)(`Profile: ${profileName}`);
91
91
  (0, output_1.printKeyValue)([
92
92
  ['baseUrl', config.baseUrl],
93
- ['token', (0, output_1.truncateToken)(config.token) || '(未设置)'],
93
+ ['token', (0, output_1.truncateToken)((0, config_1.getAccessToken)(config)) || '(未设置)'],
94
94
  ['insecure', String(config.insecure)],
95
95
  ]);
96
96
  });
@@ -130,7 +130,10 @@ function registerProfileCommand(program) {
130
130
  }
131
131
  const baseUrl = options.baseUrl || '';
132
132
  const token = options.token || '';
133
- (0, config_1.addProfile)(name, { baseUrl, token, insecure: !!options.insecure });
133
+ const auth = {
134
+ access_token: token,
135
+ };
136
+ (0, config_1.addProfile)(name, { baseUrl, auth, insecure: !!options.insecure });
134
137
  if (options.setDefault) {
135
138
  (0, config_1.setDefaultProfile)(name);
136
139
  (0, config_1.setActiveProfile)(name);
@@ -0,0 +1,286 @@
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.registerServiceLogCommand = registerServiceLogCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const service_log_1 = require("@/api/service-log");
9
+ const output_1 = require("@/utils/output");
10
+ const exit_code_1 = require("@/constants/exit-code");
11
+ const common_1 = require("@/utils/common");
12
+ const config_1 = require("@/utils/config");
13
+ const DATETIME_REGEX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
14
+ function pad2(n) {
15
+ return n < 10 ? `0${n}` : String(n);
16
+ }
17
+ function formatDateTime(d) {
18
+ return (`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ` +
19
+ `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`);
20
+ }
21
+ function colorizeLevel(level) {
22
+ const upper = (level || '').toUpperCase();
23
+ if (upper === 'ERROR' || upper === 'FATAL')
24
+ return chalk_1.default.red(level);
25
+ if (upper === 'WARN' || upper === 'WARNING')
26
+ return chalk_1.default.yellow(level);
27
+ if (upper === 'INFO')
28
+ return chalk_1.default.green(level);
29
+ if (upper === 'DEBUG' || upper === 'TRACE')
30
+ return chalk_1.default.gray(level);
31
+ return level;
32
+ }
33
+ function pickLogTime(entry) {
34
+ return entry.log_time || entry.timestamp || '-';
35
+ }
36
+ function flattenMessage(message) {
37
+ return (message ?? '').replace(/\r?\n/g, ' ');
38
+ }
39
+ function registerServiceLogCommand(program) {
40
+ const cmd = program.command('service-log').description('运维日志查询');
41
+ registerSearch(cmd);
42
+ registerStream(cmd);
43
+ }
44
+ function registerSearch(serviceLogCmd) {
45
+ serviceLogCmd
46
+ .command('search')
47
+ .description('查询历史运维日志')
48
+ .option('--time-range <r>', '相对时间范围,格式: <N>s|m|h|d(如 30s, 15m, 3h, 7d)(与 --start-time 互斥)')
49
+ .option('--start-time <time>', '开始时间 yyyy-MM-dd HH:mm:ss(与 --time-range 互斥)')
50
+ .option('--end-time <time>', '结束时间 yyyy-MM-dd HH:mm:ss(不传则服务端用请求时刻)')
51
+ .option('--limit <n>', '最大返回条数(默认 50,上限 500)', '50')
52
+ .option('--level <level>', '日志级别: INFO / WARN / ERROR')
53
+ .option('--request-id <id>', '链路 trace id(精确匹配)')
54
+ .option('--keyword <text>', '日志正文关键词')
55
+ .option('--service-type <type>', '服务类型: iam / acm / ncm / adm / mdm / nginx 等')
56
+ .option('--json', '以 JSON 输出')
57
+ .action(async (options) => {
58
+ try {
59
+ if (options.json)
60
+ (0, output_1.setJsonMode)(true);
61
+ const req = buildSearchRequest(options);
62
+ if (req instanceof Error) {
63
+ if (options.json) {
64
+ (0, output_1.printStructuredResult)({ ok: false, message: req.message });
65
+ }
66
+ else {
67
+ (0, output_1.printError)(req.message);
68
+ }
69
+ process.exitCode = exit_code_1.ExitCode.FAILURE;
70
+ return;
71
+ }
72
+ if (!options.json) {
73
+ (0, output_1.printInfo)('正在查询历史日志...');
74
+ }
75
+ const logs = await (0, service_log_1.searchServiceLogs)(req);
76
+ if (options.json) {
77
+ (0, output_1.printStructuredResult)({ ok: true, data: logs, total: logs.length });
78
+ return;
79
+ }
80
+ if (logs.length === 0) {
81
+ (0, output_1.printInfo)('暂无符合条件的日志');
82
+ return;
83
+ }
84
+ (0, output_1.printHeader)(`运维日志 (共 ${logs.length} 条,按时间倒序)`);
85
+ const rows = logs.map((entry) => [
86
+ pickLogTime(entry),
87
+ colorizeLevel(entry.level || '-'),
88
+ String(entry.host ?? '-'),
89
+ String(entry.trace_id ?? '-'),
90
+ flattenMessage(entry.message),
91
+ ]);
92
+ (0, output_1.printTable)(['时间', '级别', '服务实例', 'Trace', '消息'], rows, {
93
+ wrapCols: [4],
94
+ });
95
+ }
96
+ catch (error) {
97
+ if (options.json) {
98
+ (0, output_1.printStructuredResult)({ ok: false, message: error.message });
99
+ }
100
+ else {
101
+ (0, output_1.printError)(`查询日志失败: ${error.message}`);
102
+ }
103
+ process.exitCode = exit_code_1.ExitCode.FAILURE;
104
+ }
105
+ });
106
+ }
107
+ function buildSearchRequest(options) {
108
+ const hasRange = Boolean(options.timeRange);
109
+ const hasStart = Boolean(options.startTime);
110
+ if (!hasRange && !hasStart) {
111
+ return new Error('必须指定 --time-range 或 --start-time 之一');
112
+ }
113
+ if (hasRange && hasStart) {
114
+ return new Error('--time-range 与 --start-time 不可同时使用');
115
+ }
116
+ let start_time;
117
+ let end_time;
118
+ if (hasRange) {
119
+ const rangeKey = String(options.timeRange);
120
+ const ms = (0, common_1.parseTimeRange)(rangeKey);
121
+ if (ms === null) {
122
+ return new Error(`不支持的 --time-range 格式: "${rangeKey}",应为 <N>s|m|h|d(如 30s, 15m, 3h, 7d)`);
123
+ }
124
+ const now = new Date();
125
+ start_time = formatDateTime(new Date(now.getTime() - ms));
126
+ end_time = formatDateTime(now);
127
+ }
128
+ else {
129
+ const st = String(options.startTime);
130
+ if (!DATETIME_REGEX.test(st)) {
131
+ return new Error('--start-time 格式应为 yyyy-MM-dd HH:mm:ss');
132
+ }
133
+ start_time = st;
134
+ if (options.endTime) {
135
+ const et = String(options.endTime);
136
+ if (!DATETIME_REGEX.test(et)) {
137
+ return new Error('--end-time 格式应为 yyyy-MM-dd HH:mm:ss');
138
+ }
139
+ end_time = et;
140
+ }
141
+ }
142
+ const limit = Number(options.limit);
143
+ if (!Number.isFinite(limit) || limit <= 0) {
144
+ return new Error('--limit 必须为正整数');
145
+ }
146
+ const req = { start_time, limit };
147
+ if (end_time)
148
+ req.end_time = end_time;
149
+ if (options.level)
150
+ req.log_level = String(options.level);
151
+ if (options.requestId)
152
+ req.request_id = String(options.requestId);
153
+ if (options.keyword)
154
+ req.keyword = String(options.keyword);
155
+ if (options.serviceType)
156
+ req.service_type = String(options.serviceType);
157
+ return req;
158
+ }
159
+ function registerStream(serviceLogCmd) {
160
+ serviceLogCmd
161
+ .command('stream')
162
+ .description('实时跟随运维日志(WebSocket,Ctrl+C 退出)')
163
+ .option('--level <level>', '日志级别: INFO / WARN / ERROR')
164
+ .option('--request-id <id>', '链路 trace id(精确匹配)')
165
+ .option('--keyword <text>', '日志正文关键词')
166
+ .option('--service-type <type>', '服务类型: iam / acm / ncm / adm / mdm / nginx 等')
167
+ .option('--limit <n>', '单次 ES 拉取批量(默认 20)', '20')
168
+ .option('--max <n>', '收到 N 条后自动退出(默认不限)')
169
+ .option('--json', 'NDJSON 输出(每行一条 { ok, data })')
170
+ .action(async (options) => {
171
+ if (options.json)
172
+ (0, output_1.setJsonMode)(true);
173
+ const cfg = (0, config_1.loadConfig)();
174
+ if (!cfg.auth || !cfg.auth.access_token) {
175
+ const msg = '未登录,请先执行 ai-flow auth login';
176
+ if (options.json) {
177
+ (0, output_1.printStructuredResult)({ ok: false, message: msg });
178
+ }
179
+ else {
180
+ (0, output_1.printError)(msg);
181
+ }
182
+ process.exitCode = exit_code_1.ExitCode.FAILURE;
183
+ return;
184
+ }
185
+ try {
186
+ await (0, service_log_1.verifyToken)();
187
+ }
188
+ catch (error) {
189
+ const msg = error.message || 'Token 校验失败';
190
+ if (options.json) {
191
+ (0, output_1.printStructuredResult)({ ok: false, message: msg });
192
+ }
193
+ else {
194
+ (0, output_1.printError)(`Token 不可用: ${msg}`);
195
+ }
196
+ process.exitCode = exit_code_1.ExitCode.FAILURE;
197
+ return;
198
+ }
199
+ const filters = buildStreamFilters(options);
200
+ if (filters instanceof Error) {
201
+ if (options.json) {
202
+ (0, output_1.printStructuredResult)({ ok: false, message: filters.message });
203
+ }
204
+ else {
205
+ (0, output_1.printError)(filters.message);
206
+ }
207
+ process.exitCode = exit_code_1.ExitCode.FAILURE;
208
+ return;
209
+ }
210
+ const maxCount = options.max ? Number(options.max) : 0;
211
+ if (options.max && (!Number.isFinite(maxCount) || maxCount <= 0)) {
212
+ const msg = '--max 必须为正整数';
213
+ if (options.json) {
214
+ (0, output_1.printStructuredResult)({ ok: false, message: msg });
215
+ }
216
+ else {
217
+ (0, output_1.printError)(msg);
218
+ }
219
+ process.exitCode = exit_code_1.ExitCode.FAILURE;
220
+ return;
221
+ }
222
+ let received = 0;
223
+ let closed = false;
224
+ if (!options.json) {
225
+ (0, output_1.printInfo)('正在连接实时日志... (Ctrl+C 退出)');
226
+ }
227
+ const control = (0, service_log_1.openServiceLogStream)(filters, {
228
+ onLog: (entry) => {
229
+ received += 1;
230
+ if (options.json) {
231
+ (0, output_1.printStructuredResult)({ ok: true, data: entry });
232
+ }
233
+ else {
234
+ const time = pickLogTime(entry);
235
+ const level = colorizeLevel(entry.level || '-');
236
+ const host = entry.host ?? '-';
237
+ const msg = flattenMessage(entry.message);
238
+ console.log(`[${time}] [${level}] [${host}] ${msg}`);
239
+ }
240
+ if (maxCount > 0 && received >= maxCount) {
241
+ control.cancel();
242
+ }
243
+ },
244
+ onError: (message) => {
245
+ if (options.json) {
246
+ (0, output_1.printStructuredResult)({ ok: false, message });
247
+ }
248
+ else {
249
+ (0, output_1.printWarning)(message);
250
+ }
251
+ },
252
+ onClose: (code) => {
253
+ if (closed)
254
+ return;
255
+ closed = true;
256
+ if (!options.json) {
257
+ (0, output_1.printInfo)(`实时日志连接已关闭 (code=${code})`);
258
+ }
259
+ },
260
+ });
261
+ const onSigInt = () => {
262
+ if (!closed) {
263
+ control.cancel();
264
+ }
265
+ process.removeListener('SIGINT', onSigInt);
266
+ };
267
+ process.on('SIGINT', onSigInt);
268
+ });
269
+ }
270
+ function buildStreamFilters(options) {
271
+ const limit = Number(options.limit);
272
+ if (!Number.isFinite(limit) || limit <= 0) {
273
+ return new Error('--limit 必须为正整数');
274
+ }
275
+ const filters = { limit };
276
+ if (options.level)
277
+ filters.log_level = String(options.level);
278
+ if (options.requestId)
279
+ filters.request_id = String(options.requestId);
280
+ if (options.keyword)
281
+ filters.keyword = String(options.keyword);
282
+ if (options.serviceType)
283
+ filters.service_type = String(options.serviceType);
284
+ return filters;
285
+ }
286
+ //# sourceMappingURL=service-log.js.map
package/dist/index.js CHANGED
@@ -57,6 +57,7 @@ const link_1 = require("@/commands/link");
57
57
  const sync_1 = require("@/commands/sync");
58
58
  const org_1 = require("@/commands/org");
59
59
  const profile_1 = require("@/commands/profile");
60
+ const service_log_1 = require("@/commands/service-log");
60
61
  const updateCheck_1 = require("@/utils/updateCheck");
61
62
  const output_1 = require("@/utils/output");
62
63
  const config_2 = require("@/utils/config");
@@ -76,6 +77,7 @@ async function main() {
76
77
  (0, sync_1.registerSyncCommand)(program);
77
78
  (0, org_1.registerOrgCommand)(program);
78
79
  (0, profile_1.registerProfileCommand)(program);
80
+ (0, service_log_1.registerServiceLogCommand)(program);
79
81
  program.hook('preAction', (thisCommand) => {
80
82
  const opts = thisCommand.optsWithGlobals();
81
83
  if (opts.quiet) {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.safeParseJSON = safeParseJSON;
4
+ exports.parseTimeRange = parseTimeRange;
4
5
  function safeParseJSON(value, fallback) {
5
6
  if (typeof value !== 'string')
6
7
  return fallback;
@@ -11,4 +12,19 @@ function safeParseJSON(value, fallback) {
11
12
  return fallback;
12
13
  }
13
14
  }
15
+ const UNIT_MS = {
16
+ s: 1000,
17
+ m: 60 * 1000,
18
+ h: 60 * 60 * 1000,
19
+ d: 24 * 60 * 60 * 1000,
20
+ };
21
+ function parseTimeRange(value) {
22
+ const trimmed = String(value).trim();
23
+ const match = trimmed.match(/^(\d+)([smhd])$/i);
24
+ if (!match)
25
+ return null;
26
+ const num = parseInt(match[1], 10);
27
+ const unit = match[2].toLowerCase();
28
+ return num * (UNIT_MS[unit] ?? 0);
29
+ }
14
30
  //# sourceMappingURL=common.js.map
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getAccessToken = getAccessToken;
36
37
  exports.setActiveProfile = setActiveProfile;
37
38
  exports.getActiveProfile = getActiveProfile;
38
39
  exports.loadConfig = loadConfig;
@@ -55,8 +56,8 @@ const PROJECT_RC = '.ai-flowrc';
55
56
  const DEFAULT_PROFILE_NAME = 'default';
56
57
  const DEFAULT_CONFIG = {
57
58
  baseUrl: 'https://poc.digitalsee.cn',
58
- token: '',
59
59
  insecure: false,
60
+ auth: null,
60
61
  };
61
62
  let activeProfileOverride = null;
62
63
  function ensureConfigDir() {
@@ -88,12 +89,18 @@ function buildProfileStoreFromFlat(raw) {
88
89
  profiles: {
89
90
  [DEFAULT_PROFILE_NAME]: {
90
91
  baseUrl: raw.baseUrl || DEFAULT_CONFIG.baseUrl,
91
- token: raw.token ?? DEFAULT_CONFIG.token,
92
+ auth: raw.auth ?? (raw.token ? { access_token: raw.token } : DEFAULT_CONFIG.auth),
92
93
  insecure: raw.insecure ?? DEFAULT_CONFIG.insecure,
93
94
  },
94
95
  },
95
96
  };
96
97
  }
98
+ /**
99
+ * 从 FlowConfig 中提取 access_token,不存在时返回空字符串。
100
+ */
101
+ function getAccessToken(config) {
102
+ return config.auth?.access_token ?? '';
103
+ }
97
104
  function setActiveProfile(name) {
98
105
  activeProfileOverride = name;
99
106
  try {
@@ -219,8 +219,8 @@ function createAuthServer() {
219
219
  return;
220
220
  }
221
221
  const port = addr.port;
222
- const waitForCode = createWaitForCode(server, 'http', port);
223
- resolve({ server, port, waitForCode });
222
+ const waitForResult = createWaitForCode(server, 'http', port);
223
+ resolve({ server, port, waitForResult });
224
224
  });
225
225
  });
226
226
  }
@@ -236,8 +236,8 @@ function createAuthServerHttps() {
236
236
  return;
237
237
  }
238
238
  const port = addr.port;
239
- const waitForCode = createWaitForCode(server, 'https', port);
240
- resolve({ server, port, waitForCode });
239
+ const waitForResult = createWaitForCode(server, 'https', port);
240
+ resolve({ server, port, waitForResult });
241
241
  });
242
242
  });
243
243
  }
@@ -300,14 +300,22 @@ function createWaitForCode(server, protocol, _port) {
300
300
  server.on('request', (req, res) => {
301
301
  const url = new URL(req.url || '/', `${protocol}://127.0.0.1`);
302
302
  const error = url.searchParams.get('error');
303
- const ak = url.searchParams.get('ak');
304
- if (!ak && !error) {
303
+ const accessToken = url.searchParams.get('access_token');
304
+ const refreshToken = url.searchParams.get('refresh_token');
305
+ const idToken = url.searchParams.get('id_token');
306
+ const tokenType = url.searchParams.get('token_type');
307
+ const expiresIn = url.searchParams.get('expires_in');
308
+ if (!accessToken && !error) {
305
309
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
306
- const str = "`${location.href.substring(0, location.href.indexOf('#'))}?ak=${token}`";
310
+ const str = "`${location.href.substring(0, location.href.indexOf('#'))}?access_token=${token}&refresh_token=${refresh_token}&id_token=${id_token}&token_type=${token_type}&expires_in=${expires_in}`";
307
311
  res.end(renderPage('等待授权回调', `<p>正在等待浏览器授权,请勿关闭此页面...</p><div class="spinner"></div>`, '').replace('</body>', `<script>
308
312
  (function(){
309
313
  var params = new URLSearchParams(location.hash.substring(1));
310
314
  var token = params.get('access_token');
315
+ var refresh_token = params.get('refresh_token');
316
+ var id_token = params.get('id_token');
317
+ var token_type = params.get('token_type');
318
+ var expires_in = params.get('expires_in');
311
319
  location.href = ${str};
312
320
  })();
313
321
  </script></body>`));
@@ -325,7 +333,18 @@ function createWaitForCode(server, protocol, _port) {
325
333
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
326
334
  res.end(renderPage('登录成功', '<p>请关闭此页面返回终端</p>', '').replace('card">', 'card success">'));
327
335
  server.close();
328
- resolveCode(ak);
336
+ const result = {
337
+ access_token: accessToken ?? '',
338
+ refresh_token: refreshToken ?? '',
339
+ id_token: idToken ?? '',
340
+ token_type: tokenType ?? '',
341
+ expires_in: expiresIn ? Number(expiresIn) : 0,
342
+ };
343
+ if (!accessToken) {
344
+ rejectCode(new Error('授权回调缺少 access_token'));
345
+ return;
346
+ }
347
+ resolveCode(result);
329
348
  });
330
349
  server.on('error', (err) => {
331
350
  clearTimeout(timer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "digitalsee-ai-flow-cli",
3
- "version": "0.7.6",
3
+ "version": "0.7.23-beta.1",
4
4
  "description": "AI Flow CLI - manage connection flows and node configurations",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -17,12 +17,14 @@
17
17
  "axios": "^1.18.0",
18
18
  "chalk": "^4.1.2",
19
19
  "commander": "^12.1.0",
20
- "form-data": "^4.0.6"
20
+ "form-data": "^4.0.6",
21
+ "ws": "^8.21.1"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@commitlint/cli": "^21.0.2",
24
25
  "@commitlint/config-conventional": "^21.0.2",
25
26
  "@types/node": "^20.14.0",
27
+ "@types/ws": "^8.18.1",
26
28
  "@typescript-eslint/eslint-plugin": "^8.60.1",
27
29
  "@typescript-eslint/parser": "^8.60.1",
28
30
  "eslint": "^10.4.1",