@blynkai/cli 0.1.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 +295 -0
- package/package.json +23 -0
- package/src/cli/auth-login.mjs +44 -0
- package/src/cli/help.mjs +53 -0
- package/src/cli/index.mjs +554 -0
- package/src/cli/search-options.mjs +283 -0
- package/src/core/config.mjs +65 -0
- package/src/core/errors.mjs +25 -0
- package/src/core/response.mjs +30 -0
- package/src/core/runtime-paths.mjs +6 -0
- package/src/core/session-store.mjs +144 -0
- package/src/data/commands.mjs +37 -0
- package/src/search/normalize-payload.mjs +201 -0
- package/src/server/index.mjs +308 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
const RISK_ALIAS = {
|
|
2
|
+
normal: 'E0-Normal',
|
|
3
|
+
e0: 'E0-Normal',
|
|
4
|
+
low: 'E1-Low',
|
|
5
|
+
e1: 'E1-Low',
|
|
6
|
+
medium: 'E2-Medium',
|
|
7
|
+
mid: 'E2-Medium',
|
|
8
|
+
e2: 'E2-Medium',
|
|
9
|
+
high: 'E3-High',
|
|
10
|
+
e3: 'E3-High',
|
|
11
|
+
very_high: 'E4-VeryHigh',
|
|
12
|
+
veryhigh: 'E4-VeryHigh',
|
|
13
|
+
critical: 'E4-VeryHigh',
|
|
14
|
+
e4: 'E4-VeryHigh',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SENTIMENT_ALIAS = {
|
|
18
|
+
positive: 'Positive',
|
|
19
|
+
pos: 'Positive',
|
|
20
|
+
neutral: 'Neutral',
|
|
21
|
+
neu: 'Neutral',
|
|
22
|
+
negative: 'Negative',
|
|
23
|
+
neg: 'Negative',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const FIELD_ALIAS = {
|
|
27
|
+
title: 'title',
|
|
28
|
+
content: 'content',
|
|
29
|
+
author: 'author',
|
|
30
|
+
ocr: 'image_ocr_content',
|
|
31
|
+
image_ocr: 'image_ocr_content',
|
|
32
|
+
image_ocr_content: 'image_ocr_content',
|
|
33
|
+
asr: 'video_ocr_content',
|
|
34
|
+
video_ocr: 'video_ocr_content',
|
|
35
|
+
video_ocr_content: 'video_ocr_content',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ARTICLE_TYPE_ALIAS = {
|
|
39
|
+
original: 1,
|
|
40
|
+
repost: 2,
|
|
41
|
+
forward: 2,
|
|
42
|
+
comment: 3,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const VALID_ALIAS = {
|
|
46
|
+
yes: 'Valid',
|
|
47
|
+
valid: 'Valid',
|
|
48
|
+
no: 'Invalid',
|
|
49
|
+
invalid: 'Invalid',
|
|
50
|
+
uncertain: 'Uncertain',
|
|
51
|
+
unsure: 'Uncertain',
|
|
52
|
+
all: '',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const READ_ALIAS = {
|
|
56
|
+
read: 'read',
|
|
57
|
+
unread: 'unprocessed',
|
|
58
|
+
unprocessed: 'unprocessed',
|
|
59
|
+
all: '',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function normalizeSearchScene(scene) {
|
|
63
|
+
const aliases = {
|
|
64
|
+
data: 'data-center',
|
|
65
|
+
datacenter: 'data-center',
|
|
66
|
+
'data-center': 'data-center',
|
|
67
|
+
warning: 'warning-center',
|
|
68
|
+
warningcenter: 'warning-center',
|
|
69
|
+
'warning-center': 'warning-center',
|
|
70
|
+
website: 'website-stats',
|
|
71
|
+
websites: 'website-stats',
|
|
72
|
+
'website-stats': 'website-stats',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return aliases[scene] ?? scene
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeCenter(value) {
|
|
79
|
+
const item = firstValue(value)
|
|
80
|
+
if (!item) return 'data'
|
|
81
|
+
|
|
82
|
+
const aliases = {
|
|
83
|
+
data: 'data',
|
|
84
|
+
datacenter: 'data',
|
|
85
|
+
'data-center': 'data',
|
|
86
|
+
article: 'data',
|
|
87
|
+
articles: 'data',
|
|
88
|
+
warning: 'warning',
|
|
89
|
+
warningcenter: 'warning',
|
|
90
|
+
'warning-center': 'warning',
|
|
91
|
+
alert: 'warning',
|
|
92
|
+
alerts: 'warning',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return aliases[item.toLowerCase()] ?? item
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeStats(value) {
|
|
99
|
+
const item = firstValue(value)
|
|
100
|
+
if (!item) return ''
|
|
101
|
+
|
|
102
|
+
const aliases = {
|
|
103
|
+
website: 'website',
|
|
104
|
+
websites: 'website',
|
|
105
|
+
site: 'website',
|
|
106
|
+
sites: 'website',
|
|
107
|
+
channel: 'website',
|
|
108
|
+
channels: 'website',
|
|
109
|
+
none: '',
|
|
110
|
+
false: '',
|
|
111
|
+
no: '',
|
|
112
|
+
'0': '',
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return aliases[item.toLowerCase()] ?? item
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function parseFlags(args) {
|
|
119
|
+
const flags = {}
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
122
|
+
const token = args[i]
|
|
123
|
+
if (!token.startsWith('-')) continue
|
|
124
|
+
|
|
125
|
+
const key = token.replace(/^-+/, '')
|
|
126
|
+
const values = []
|
|
127
|
+
|
|
128
|
+
while (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
129
|
+
values.push(args[i + 1])
|
|
130
|
+
i += 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
flags[key] = values.length === 0 ? true : values.length === 1 ? values[0] : values
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return flags
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function parseSearchArgs(args) {
|
|
140
|
+
const flags = parseFlags(args)
|
|
141
|
+
const terms = []
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
144
|
+
const token = args[i]
|
|
145
|
+
|
|
146
|
+
if (token.startsWith('-')) {
|
|
147
|
+
while (args[i + 1] && !args[i + 1].startsWith('-')) i += 1
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const eqIndex = token.indexOf('=')
|
|
152
|
+
if (eqIndex > 0) {
|
|
153
|
+
const key = token.slice(0, eqIndex).trim()
|
|
154
|
+
const value = token.slice(eqIndex + 1).trim()
|
|
155
|
+
if (key) flags[key] = value
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
terms.push(token)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { flags, terms }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getFlag(flags, ...keys) {
|
|
166
|
+
for (const key of keys) {
|
|
167
|
+
if (flags[key] !== undefined) return flags[key]
|
|
168
|
+
}
|
|
169
|
+
return undefined
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toArray(value) {
|
|
173
|
+
if (value === undefined || value === null || value === true) return []
|
|
174
|
+
return Array.isArray(value) ? value : [value]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function splitValues(value) {
|
|
178
|
+
return toArray(value)
|
|
179
|
+
.flatMap((item) => String(item).split(','))
|
|
180
|
+
.map((item) => item.trim())
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function firstValue(value) {
|
|
185
|
+
return splitValues(value)[0]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toNumber(value) {
|
|
189
|
+
if (value === undefined || value === null || value === true || value === '') return undefined
|
|
190
|
+
const number = Number(value)
|
|
191
|
+
return Number.isFinite(number) ? number : undefined
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mapValues(value, aliases) {
|
|
195
|
+
return splitValues(value).map((item) => aliases[item.toLowerCase()] ?? item)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function mapFirstValue(value, aliases) {
|
|
199
|
+
const item = firstValue(value)
|
|
200
|
+
if (!item) return undefined
|
|
201
|
+
return aliases[item.toLowerCase()] ?? item
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function toBoolean(value) {
|
|
205
|
+
if (value === true) return true
|
|
206
|
+
const item = firstValue(value)
|
|
207
|
+
if (!item) return false
|
|
208
|
+
return ['true', 'yes', '1', 'on'].includes(item.toLowerCase())
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function buildSearchBody(scene, flags) {
|
|
212
|
+
const center = normalizeCenter(getFlag(flags, 'center', 'scope', 'target') ?? scene)
|
|
213
|
+
const stats = normalizeStats(getFlag(flags, 'stats', 'stat', 'aggregate', 'aggregation'))
|
|
214
|
+
const searchMode = getFlag(flags, 'mode', 'search-mode', 'searchMode') ?? 'keyword'
|
|
215
|
+
const page = toNumber(getFlag(flags, 'page'))
|
|
216
|
+
const size = toNumber(getFlag(flags, 'size', 'limit'))
|
|
217
|
+
const semanticTop = toNumber(getFlag(flags, 'semantic_n', 'semantic-top', 'semanticTop'))
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
scene:
|
|
221
|
+
stats === 'website'
|
|
222
|
+
? 'website-stats'
|
|
223
|
+
: center === 'warning'
|
|
224
|
+
? 'warning-center'
|
|
225
|
+
: 'data-center',
|
|
226
|
+
center,
|
|
227
|
+
time: {
|
|
228
|
+
start: getFlag(flags, 'time_s', 'time-start', 'timeStart'),
|
|
229
|
+
end: getFlag(flags, 'time_e', 'time-end', 'timeEnd'),
|
|
230
|
+
publish_start: getFlag(flags, 'publish_s', 'publish-start', 'publishStart'),
|
|
231
|
+
publish_end: getFlag(flags, 'publish_e', 'publish-end', 'publishEnd'),
|
|
232
|
+
push_start: getFlag(flags, 'push_s', 'push-start', 'pushStart'),
|
|
233
|
+
push_end: getFlag(flags, 'push_e', 'push-end', 'pushEnd'),
|
|
234
|
+
},
|
|
235
|
+
query: {
|
|
236
|
+
keywords: getFlag(flags, 'q', 'keywords', 'keyword', 'kw', 'k') ?? '',
|
|
237
|
+
keywords_must_include: splitValues(getFlag(flags, 'must', 'must_include', 'must-include', 'mustInclude')),
|
|
238
|
+
keywords_should_include: splitValues(getFlag(flags, 'should', 'should_include', 'should-include', 'shouldInclude')),
|
|
239
|
+
keywords_must_exclude: splitValues(getFlag(flags, 'exclude', 'must_exclude', 'must-exclude', 'mustExclude')),
|
|
240
|
+
search_fields: mapValues(getFlag(flags, 'field', 'fields', 'search-fields', 'search-field', 'searchFields'), FIELD_ALIAS),
|
|
241
|
+
search_mode: searchMode,
|
|
242
|
+
semantic_query: getFlag(flags, 'semantic_q', 'semantic-query', 'semanticQuery') ?? null,
|
|
243
|
+
semantic_top_n: semanticTop ?? null,
|
|
244
|
+
},
|
|
245
|
+
filters: {
|
|
246
|
+
article_categories: splitValues(getFlag(flags, 'category', 'cat', 'categories', 'c')),
|
|
247
|
+
media_channels: splitValues(getFlag(flags, 'channel', 'channels')),
|
|
248
|
+
website_names: splitValues(getFlag(flags, 'site', 'sites', 'website', 'websites')),
|
|
249
|
+
sentiment: mapValues(getFlag(flags, 'sentiment', 's'), SENTIMENT_ALIAS),
|
|
250
|
+
risk_levels: mapValues(getFlag(flags, 'risk', 'risk-levels', 'risk-level', 'riskLevels', 'r'), RISK_ALIAS),
|
|
251
|
+
media_tags: splitValues(getFlag(flags, 'media_tag', 'media_tags', 'media-tags', 'media-tag', 'mediaTags')),
|
|
252
|
+
article_type: mapValues(getFlag(flags, 'type', 'article_type', 'article-type', 'articleType'), ARTICLE_TYPE_ALIAS)
|
|
253
|
+
.map((item) => Number(item))
|
|
254
|
+
.filter((item) => Number.isFinite(item)),
|
|
255
|
+
article_id_list: splitValues(getFlag(flags, 'id', 'ids', 'article_id', 'article_ids', 'article-id-list', 'article-id', 'articleIds')),
|
|
256
|
+
article_status: getFlag(flags, 'article_status', 'article-status', 'articleStatus') ?? '',
|
|
257
|
+
is_valid: mapFirstValue(getFlag(flags, 'valid', 'is-valid', 'isValid'), VALID_ALIAS),
|
|
258
|
+
status: mapFirstValue(getFlag(flags, 'read', 'status'), READ_ALIAS),
|
|
259
|
+
review_status: getFlag(flags, 'review', 'review_status', 'review-status', 'reviewStatus'),
|
|
260
|
+
reviewer: getFlag(flags, 'reviewer'),
|
|
261
|
+
third_party: splitValues(getFlag(flags, 'source', 'third_party', 'third-party', 'thirdParty')),
|
|
262
|
+
third_party_id: getFlag(flags, 'source_id', 'third_party_id', 'third-party-id', 'thirdPartyId') ?? null,
|
|
263
|
+
collapse_field: getFlag(flags, 'collapse', 'collapse_field', 'collapse-field', 'collapseField') ?? '',
|
|
264
|
+
is_similar_collapse: toBoolean(getFlag(flags, 'similar', 'similar_collapse', 'similar-collapse', 'is-similar-collapse')),
|
|
265
|
+
topic_id: getFlag(flags, 'topic', 'topic_id', 'topic-id', 'topicId'),
|
|
266
|
+
warning_cluster_id: getFlag(flags, 'cluster', 'cluster_id', 'cluster-id', 'warning-cluster-id', 'warningClusterId'),
|
|
267
|
+
exclude_ids: splitValues(getFlag(flags, 'exclude_id', 'exclude_ids', 'exclude-ids', 'exclude-id', 'excludeIds')),
|
|
268
|
+
},
|
|
269
|
+
page: {
|
|
270
|
+
page: page ?? 1,
|
|
271
|
+
size: size ?? 10,
|
|
272
|
+
},
|
|
273
|
+
sort: {
|
|
274
|
+
field: getFlag(flags, 'sort', 'sort_field', 'sort-field', 'sortField'),
|
|
275
|
+
order: getFlag(flags, 'order', 'sort_order', 'sort-order', 'sortOrder') ?? 'desc',
|
|
276
|
+
},
|
|
277
|
+
output: {
|
|
278
|
+
dry_run: toBoolean(getFlag(flags, 'dry_run', 'dry-run', 'dryRun')),
|
|
279
|
+
stats,
|
|
280
|
+
website_stats: stats === 'website',
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
const repoRoot = resolve(__dirname, '../../..')
|
|
7
|
+
|
|
8
|
+
function parseEnvContent(content) {
|
|
9
|
+
const entries = {}
|
|
10
|
+
|
|
11
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
12
|
+
const line = rawLine.trim()
|
|
13
|
+
if (!line || line.startsWith('#')) continue
|
|
14
|
+
|
|
15
|
+
const eqIndex = line.indexOf('=')
|
|
16
|
+
if (eqIndex <= 0) continue
|
|
17
|
+
|
|
18
|
+
const key = line.slice(0, eqIndex).trim()
|
|
19
|
+
let value = line.slice(eqIndex + 1).trim()
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
23
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
24
|
+
) {
|
|
25
|
+
value = value.slice(1, -1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
entries[key] = value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return entries
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readEnvFile(path) {
|
|
35
|
+
try {
|
|
36
|
+
return parseEnvContent(readFileSync(path, 'utf8'))
|
|
37
|
+
} catch {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeBaseUrl(value) {
|
|
43
|
+
if (!value) return ''
|
|
44
|
+
return value.replace(/\/+$/, '')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveEnvValue(keys) {
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
if (process.env[key]) return process.env[key]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const envFiles = ['.env.local', '.env.production.local', '.env.production', '.env.test']
|
|
53
|
+
for (const file of envFiles) {
|
|
54
|
+
const env = readEnvFile(resolve(repoRoot, file))
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
if (env[key]) return env[key]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveUpstreamBaseUrl() {
|
|
64
|
+
return normalizeBaseUrl(resolveEnvValue(['LINGRUI_UPSTREAM_BASE_URL', 'VITE_API_BASE_URL']))
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const ErrorCode = {
|
|
2
|
+
AUTH_REQUIRED: 40010,
|
|
3
|
+
SESSION_EXPIRED: 40011,
|
|
4
|
+
SESSION_REVOKED: 40012,
|
|
5
|
+
RECONNECT_REQUIRED: 40013,
|
|
6
|
+
LOOPBACK_ONLY: 40014,
|
|
7
|
+
INVALID_PARAMS: 40020,
|
|
8
|
+
UNSUPPORTED_OPERATION: 40022,
|
|
9
|
+
NOT_IMPLEMENTED: 40024,
|
|
10
|
+
NOT_FOUND: 40400,
|
|
11
|
+
INTERNAL_ERROR: 50010,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ErrorMessage = {
|
|
15
|
+
[ErrorCode.AUTH_REQUIRED]: 'auth_required',
|
|
16
|
+
[ErrorCode.SESSION_EXPIRED]: 'session_expired',
|
|
17
|
+
[ErrorCode.SESSION_REVOKED]: 'session_revoked',
|
|
18
|
+
[ErrorCode.RECONNECT_REQUIRED]: 'reconnect_required',
|
|
19
|
+
[ErrorCode.LOOPBACK_ONLY]: 'loopback_only',
|
|
20
|
+
[ErrorCode.INVALID_PARAMS]: 'invalid_params',
|
|
21
|
+
[ErrorCode.UNSUPPORTED_OPERATION]: 'unsupported_operation',
|
|
22
|
+
[ErrorCode.NOT_IMPLEMENTED]: 'not_implemented',
|
|
23
|
+
[ErrorCode.NOT_FOUND]: 'not_found',
|
|
24
|
+
[ErrorCode.INTERNAL_ERROR]: 'internal_error',
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ErrorMessage } from './errors.mjs'
|
|
2
|
+
|
|
3
|
+
export function ok(data = {}) {
|
|
4
|
+
return {
|
|
5
|
+
code: 0,
|
|
6
|
+
msg: 'ok',
|
|
7
|
+
data,
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function fail(code, details = undefined) {
|
|
12
|
+
return {
|
|
13
|
+
code,
|
|
14
|
+
msg: ErrorMessage[code] ?? 'unknown_error',
|
|
15
|
+
...(details ? { details } : {}),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sendJson(res, statusCode, payload) {
|
|
20
|
+
const body = JSON.stringify(payload, null, 2)
|
|
21
|
+
res.writeHead(statusCode, {
|
|
22
|
+
'content-type': 'application/json; charset=utf-8',
|
|
23
|
+
'content-length': Buffer.byteLength(body),
|
|
24
|
+
'access-control-allow-origin': '*',
|
|
25
|
+
'access-control-allow-methods': 'GET,POST,PATCH,PUT,DELETE,OPTIONS',
|
|
26
|
+
'access-control-allow-headers': 'content-type,x-request-id',
|
|
27
|
+
'access-control-allow-private-network': 'true',
|
|
28
|
+
})
|
|
29
|
+
res.end(body)
|
|
30
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
4
|
+
import { runtimeDir } from './runtime-paths.mjs'
|
|
5
|
+
|
|
6
|
+
const dataDir = join(runtimeDir, 'data')
|
|
7
|
+
const sessionFile = join(dataDir, 'session.json')
|
|
8
|
+
|
|
9
|
+
let cachedSession = null
|
|
10
|
+
let cachedSessionMtimeMs = 0
|
|
11
|
+
|
|
12
|
+
async function ensureDataDir() {
|
|
13
|
+
await mkdir(dataDir, { recursive: true })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function persistSession(session) {
|
|
17
|
+
await ensureDataDir()
|
|
18
|
+
const persisted = {
|
|
19
|
+
...session,
|
|
20
|
+
token: session.token
|
|
21
|
+
? {
|
|
22
|
+
present: session.token.present,
|
|
23
|
+
masked: session.token.masked,
|
|
24
|
+
sha256: session.token.sha256,
|
|
25
|
+
value: session.token.value,
|
|
26
|
+
}
|
|
27
|
+
: session.token,
|
|
28
|
+
}
|
|
29
|
+
await writeFile(sessionFile, JSON.stringify(persisted, null, 2), 'utf8')
|
|
30
|
+
try {
|
|
31
|
+
const sessionStat = await stat(sessionFile)
|
|
32
|
+
cachedSessionMtimeMs = sessionStat.mtimeMs
|
|
33
|
+
} catch {
|
|
34
|
+
cachedSessionMtimeMs = 0
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function loadSession() {
|
|
39
|
+
try {
|
|
40
|
+
const sessionStat = await stat(sessionFile)
|
|
41
|
+
if (cachedSession && cachedSessionMtimeMs === sessionStat.mtimeMs) return cachedSession
|
|
42
|
+
|
|
43
|
+
const raw = await readFile(sessionFile, 'utf8')
|
|
44
|
+
cachedSession = JSON.parse(raw)
|
|
45
|
+
cachedSessionMtimeMs = sessionStat.mtimeMs
|
|
46
|
+
return cachedSession
|
|
47
|
+
} catch {
|
|
48
|
+
cachedSession = null
|
|
49
|
+
cachedSessionMtimeMs = 0
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function maskToken(token) {
|
|
55
|
+
if (!token) return ''
|
|
56
|
+
if (token.length <= 12) return `${token.slice(0, 3)}***`
|
|
57
|
+
return `${token.slice(0, 6)}***${token.slice(-6)}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hashToken(token) {
|
|
61
|
+
return createHash('sha256').update(token).digest('hex')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function syncBrowserSession(payload) {
|
|
65
|
+
const now = new Date()
|
|
66
|
+
const expiresAt = payload.expiresAt
|
|
67
|
+
? new Date(payload.expiresAt)
|
|
68
|
+
: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
|
|
69
|
+
const session = {
|
|
70
|
+
sessionId: `sess_${randomUUID()}`,
|
|
71
|
+
status: 'active',
|
|
72
|
+
createdAt: now.toISOString(),
|
|
73
|
+
updatedAt: now.toISOString(),
|
|
74
|
+
expiresAt: Number.isNaN(expiresAt.getTime())
|
|
75
|
+
? new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString()
|
|
76
|
+
: expiresAt.toISOString(),
|
|
77
|
+
scopes: Array.isArray(payload.scopes)
|
|
78
|
+
? payload.scopes
|
|
79
|
+
: typeof payload.scopes === 'string'
|
|
80
|
+
? payload.scopes.split(/[,\s]+/).filter(Boolean)
|
|
81
|
+
: ['skills:use'],
|
|
82
|
+
user: {
|
|
83
|
+
username: payload.username || '',
|
|
84
|
+
nickname: payload.nickname || '',
|
|
85
|
+
institution: payload.institution || '',
|
|
86
|
+
institutionId: payload.institutionId || '',
|
|
87
|
+
role: payload.role || 'user',
|
|
88
|
+
},
|
|
89
|
+
token: {
|
|
90
|
+
present: Boolean(payload.token),
|
|
91
|
+
masked: maskToken(payload.token),
|
|
92
|
+
sha256: payload.token ? hashToken(payload.token) : '',
|
|
93
|
+
value: payload.token || '',
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cachedSession = session
|
|
98
|
+
await persistSession(session)
|
|
99
|
+
console.log(
|
|
100
|
+
`[auth] synced browser session: user=${session.user.username || '-'} org=${
|
|
101
|
+
session.user.nickname || session.user.institution || '-'
|
|
102
|
+
} expiresAt=${session.expiresAt}`,
|
|
103
|
+
)
|
|
104
|
+
return session
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function syncCliSession(payload) {
|
|
108
|
+
return syncBrowserSession(payload)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function revokeSession() {
|
|
112
|
+
const session = await loadSession()
|
|
113
|
+
const now = new Date().toISOString()
|
|
114
|
+
|
|
115
|
+
const revoked = {
|
|
116
|
+
...(session ?? {
|
|
117
|
+
sessionId: null,
|
|
118
|
+
createdAt: now,
|
|
119
|
+
scopes: [],
|
|
120
|
+
user: null,
|
|
121
|
+
}),
|
|
122
|
+
status: 'revoked',
|
|
123
|
+
updatedAt: now,
|
|
124
|
+
revokedAt: now,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
cachedSession = revoked
|
|
128
|
+
await persistSession(revoked)
|
|
129
|
+
return revoked
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function getSessionState() {
|
|
133
|
+
const session = await loadSession()
|
|
134
|
+
if (!session) return { state: 'missing', session: null }
|
|
135
|
+
if (session.status === 'revoked') return { state: 'revoked', session }
|
|
136
|
+
|
|
137
|
+
const expiresAt = new Date(session.expiresAt).getTime()
|
|
138
|
+
if (Number.isFinite(expiresAt) && expiresAt < Date.now()) {
|
|
139
|
+
return { state: 'expired', session }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (session.status !== 'active') return { state: 'missing', session }
|
|
143
|
+
return { state: 'active', session }
|
|
144
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const availableCommands = [
|
|
2
|
+
{
|
|
3
|
+
name: 'help',
|
|
4
|
+
description: '查看命令帮助',
|
|
5
|
+
example: 'blynkai help',
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
name: 'status',
|
|
9
|
+
description: '查看 CLI 和浏览器登录状态',
|
|
10
|
+
example: 'blynkai status',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'whoami',
|
|
14
|
+
description: '查询当前浏览器登录用户',
|
|
15
|
+
example: 'blynkai whoami',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'list datacenter',
|
|
19
|
+
description: '查询数据中心列表(接口待接入)',
|
|
20
|
+
example: 'blynkai list datacenter --page 1 --limit 10',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'list fengxian',
|
|
24
|
+
description: '查询风险列表(接口待接入)',
|
|
25
|
+
example: 'blynkai list fengxian --page 1 --limit 10',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'search',
|
|
29
|
+
description: '搜索数据(接口待接入)',
|
|
30
|
+
example: 'blynkai search --kw "alibaba" --date "2026-05-01,2026-05-13"',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'logout',
|
|
34
|
+
description: '吊销本地登录会话',
|
|
35
|
+
example: 'blynkai logout',
|
|
36
|
+
},
|
|
37
|
+
]
|