@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.
@@ -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,6 @@
1
+ import { homedir } from 'node:os'
2
+ import { join } from 'node:path'
3
+
4
+ export const runtimeDir = join(homedir(), '.lingrui-local-agent')
5
+ export const logFile = join(runtimeDir, 'agent.log')
6
+ export const pidFile = join(runtimeDir, 'agent.pid')
@@ -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
+ ]