@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,554 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { openSync } from 'node:fs'
|
|
5
|
+
import { mkdir } from 'node:fs/promises'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { ErrorCode } from '../core/errors.mjs'
|
|
9
|
+
import { logFile, pidFile, runtimeDir } from '../core/runtime-paths.mjs'
|
|
10
|
+
import { runBrowserLogin } from './auth-login.mjs'
|
|
11
|
+
import { printHelp } from './help.mjs'
|
|
12
|
+
import {
|
|
13
|
+
buildSearchBody,
|
|
14
|
+
getFlag,
|
|
15
|
+
normalizeSearchScene,
|
|
16
|
+
parseFlags,
|
|
17
|
+
parseSearchArgs,
|
|
18
|
+
} from './search-options.mjs'
|
|
19
|
+
|
|
20
|
+
const preferredBaseUrl = process.env.LINGRUI_AGENT_BASE_URL ?? 'http://127.0.0.1:38100'
|
|
21
|
+
const basePort = Number(new URL(preferredBaseUrl).port || 38100)
|
|
22
|
+
let resolvedBaseUrl = process.env.LINGRUI_AGENT_BASE_URL ?? null
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
const serverEntry = join(__dirname, '../server/index.mjs')
|
|
25
|
+
const requestTimeoutMs = Number(process.env.LINGRUI_AGENT_REQUEST_TIMEOUT_MS ?? 3000)
|
|
26
|
+
|
|
27
|
+
async function fetchJson(url, options = {}) {
|
|
28
|
+
const controller = new AbortController()
|
|
29
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? requestTimeoutMs)
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(url, {
|
|
33
|
+
method: options.method ?? 'GET',
|
|
34
|
+
headers: {
|
|
35
|
+
'content-type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
...(options.body ? { body: JSON.stringify(options.body) } : {}),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return response.json()
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timeout)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function rawRequest(path, options = {}) {
|
|
48
|
+
const baseUrl = await getBaseUrl(path)
|
|
49
|
+
return fetchJson(`${baseUrl}${path}`, options)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function tryRequest(baseUrl, path, options = {}) {
|
|
53
|
+
return fetchJson(`${baseUrl}${path}`, {
|
|
54
|
+
...options,
|
|
55
|
+
timeoutMs: options.timeoutMs ?? 800,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function hasEndpoint(baseUrl, path) {
|
|
60
|
+
if (path !== '/api/search') return true
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const payload = await tryRequest(baseUrl, '/api/search', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: {
|
|
66
|
+
scene: 'data-center',
|
|
67
|
+
output: { dry_run: true },
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
return payload?.code !== 40400
|
|
71
|
+
} catch {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function getBaseUrl(path = '/health') {
|
|
77
|
+
if (resolvedBaseUrl && (await hasEndpoint(resolvedBaseUrl, path))) return resolvedBaseUrl
|
|
78
|
+
|
|
79
|
+
if (process.env.LINGRUI_AGENT_BASE_URL) {
|
|
80
|
+
resolvedBaseUrl = preferredBaseUrl
|
|
81
|
+
return resolvedBaseUrl
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let port = basePort; port <= basePort + 20; port += 1) {
|
|
85
|
+
const candidate = `http://127.0.0.1:${port}`
|
|
86
|
+
try {
|
|
87
|
+
const payload = await tryRequest(candidate, '/health')
|
|
88
|
+
if (payload?.code === 0 && payload?.data?.service === 'lingrui-local-agent') {
|
|
89
|
+
if (await hasEndpoint(candidate, path)) {
|
|
90
|
+
resolvedBaseUrl = candidate
|
|
91
|
+
return resolvedBaseUrl
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Try the next local port.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
resolvedBaseUrl = preferredBaseUrl
|
|
100
|
+
return resolvedBaseUrl
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sleep(ms) {
|
|
104
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function waitForBaseUrl(path = '/health') {
|
|
108
|
+
for (let i = 0; i < 20; i += 1) {
|
|
109
|
+
resolvedBaseUrl = null
|
|
110
|
+
const baseUrl = await getBaseUrl(path)
|
|
111
|
+
if (await hasEndpoint(baseUrl, path)) return baseUrl
|
|
112
|
+
await sleep(100)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return getBaseUrl(path)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeStatusPayload(payload) {
|
|
119
|
+
const data = payload?.data ?? {}
|
|
120
|
+
const authState = data.authState ?? 'missing'
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
code: 0,
|
|
124
|
+
msg: 'ok',
|
|
125
|
+
data: {
|
|
126
|
+
service: data.service ?? 'lingrui-cli',
|
|
127
|
+
status: data.status ?? 'online',
|
|
128
|
+
loggedIn: authState === 'active',
|
|
129
|
+
authState,
|
|
130
|
+
user: data.user ?? null,
|
|
131
|
+
...(data.port ? { port: data.port } : {}),
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getHint(msg) {
|
|
137
|
+
if (msg === 'auth_required') {
|
|
138
|
+
return '请运行 blynkai login,在浏览器中完成 CLI 授权。'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (msg === 'session_revoked') {
|
|
142
|
+
return '当前本地会话已失效。请运行 blynkai login 重新授权。'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (msg === 'local_agent_unavailable') {
|
|
146
|
+
return `无法连接到本地助手服务。请确认服务是否正常运行,或重试。`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function request(path, options = {}) {
|
|
153
|
+
try {
|
|
154
|
+
const payload = await rawRequest(path, options)
|
|
155
|
+
const msg = payload.msg ?? payload.message
|
|
156
|
+
if (payload.code !== 0) {
|
|
157
|
+
const hint = getHint(msg)
|
|
158
|
+
console.log(`\n❌ 请求失败 [错误代码: ${payload.code}]`)
|
|
159
|
+
if (hint) {
|
|
160
|
+
console.log(`💡 提示: ${hint}`)
|
|
161
|
+
} else {
|
|
162
|
+
console.log(`📄 原因: ${msg}`)
|
|
163
|
+
}
|
|
164
|
+
console.log()
|
|
165
|
+
process.exitCode = 1
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return payload
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const msg = 'local_agent_unavailable'
|
|
172
|
+
const hint = getHint(msg)
|
|
173
|
+
console.log(`\n❌ 服务未响应 [错误代码: 50010]`)
|
|
174
|
+
console.log(`💡 提示: ${hint}\n`)
|
|
175
|
+
process.exitCode = 1
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function requestStatus() {
|
|
181
|
+
try {
|
|
182
|
+
const payload = await rawRequest('/status')
|
|
183
|
+
if (payload.code === 0) return normalizeStatusPayload(payload)
|
|
184
|
+
|
|
185
|
+
if (payload.code === 40400 && payload.details?.path === '/status') {
|
|
186
|
+
const fallback = await rawRequest('/health')
|
|
187
|
+
if (fallback.code === 0) return normalizeStatusPayload(fallback)
|
|
188
|
+
return fallback
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return payload
|
|
192
|
+
} catch {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function printJson(value) {
|
|
198
|
+
console.log(JSON.stringify(value, null, 2))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function printError(code, msg, details = undefined) {
|
|
202
|
+
printJson({
|
|
203
|
+
code,
|
|
204
|
+
msg,
|
|
205
|
+
...(details ? { details } : {}),
|
|
206
|
+
})
|
|
207
|
+
process.exitCode = 1
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function toDisplayText(value, fallback = '-') {
|
|
211
|
+
if (value === undefined || value === null || value === '') return fallback
|
|
212
|
+
return String(value)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getArticleSource(item) {
|
|
216
|
+
return (
|
|
217
|
+
item.website_name ||
|
|
218
|
+
item.website ||
|
|
219
|
+
item.media_channel ||
|
|
220
|
+
item.source ||
|
|
221
|
+
item.author ||
|
|
222
|
+
'-'
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatSearchList(payload) {
|
|
227
|
+
const responseData = payload?.data?.result?.data
|
|
228
|
+
const items = Array.isArray(responseData?.items) ? responseData.items : []
|
|
229
|
+
const total = responseData?.total ?? items.length
|
|
230
|
+
|
|
231
|
+
if (items.length === 0) {
|
|
232
|
+
console.log('\n📭 未查询到结果\n')
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log(`\n🔎 查询结果:共 ${total} 条,当前 ${items.length} 条\n`)
|
|
237
|
+
|
|
238
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
239
|
+
const item = items[i]
|
|
240
|
+
const analysis = item?.analysis || {}
|
|
241
|
+
const idx = i + 1
|
|
242
|
+
const title = toDisplayText(item?.title || item?.content, '(无标题)')
|
|
243
|
+
const publishTime = toDisplayText(item?.publish_time)
|
|
244
|
+
const pushTime = toDisplayText(item?.push_time)
|
|
245
|
+
const source = toDisplayText(getArticleSource(item))
|
|
246
|
+
const risk = toDisplayText(analysis?.risk_level || analysis?.risk_level_original)
|
|
247
|
+
const sentiment = toDisplayText(analysis?.sentiment || analysis?.sentiment_original)
|
|
248
|
+
const category = toDisplayText(analysis?.category || analysis?.category_original)
|
|
249
|
+
const summary = toDisplayText(analysis?.summary)
|
|
250
|
+
const valid = toDisplayText(analysis?.is_valid)
|
|
251
|
+
|
|
252
|
+
console.log(`${idx}. ${title}`)
|
|
253
|
+
console.log(` 来源: ${source} | 发布时间: ${publishTime} | 推送时间: ${pushTime}`)
|
|
254
|
+
console.log(` 风险: ${risk} | 情感: ${sentiment} | 分类: ${category} | 相关性: ${valid}`)
|
|
255
|
+
console.log(` 摘要: ${summary}`)
|
|
256
|
+
console.log()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function runDoctor() {
|
|
263
|
+
const baseUrl = await getBaseUrl('/api/search')
|
|
264
|
+
const report = {
|
|
265
|
+
code: 0,
|
|
266
|
+
msg: 'ok',
|
|
267
|
+
data: {
|
|
268
|
+
now: new Date().toISOString(),
|
|
269
|
+
baseUrl,
|
|
270
|
+
env: {
|
|
271
|
+
LINGRUI_AGENT_BASE_URL: process.env.LINGRUI_AGENT_BASE_URL ?? '',
|
|
272
|
+
LINGRUI_UPSTREAM_BASE_URL: process.env.LINGRUI_UPSTREAM_BASE_URL ?? '',
|
|
273
|
+
NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS ?? '',
|
|
274
|
+
NODE_TLS_REJECT_UNAUTHORIZED: process.env.NODE_TLS_REJECT_UNAUTHORIZED ?? '',
|
|
275
|
+
},
|
|
276
|
+
checks: {
|
|
277
|
+
health: null,
|
|
278
|
+
whoami: null,
|
|
279
|
+
upstreamReachability: null,
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
report.data.checks.health = await tryRequest(baseUrl, '/health', { timeoutMs: 2000 })
|
|
286
|
+
} catch (error) {
|
|
287
|
+
report.data.checks.health = {
|
|
288
|
+
code: 50010,
|
|
289
|
+
msg: 'local_agent_unavailable',
|
|
290
|
+
error: error instanceof Error ? error.message : String(error),
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
report.data.checks.whoami = await tryRequest(baseUrl, '/auth/whoami', { timeoutMs: 2000 })
|
|
296
|
+
} catch (error) {
|
|
297
|
+
report.data.checks.whoami = {
|
|
298
|
+
code: 50010,
|
|
299
|
+
msg: 'local_agent_unavailable',
|
|
300
|
+
error: error instanceof Error ? error.message : String(error),
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const upstreamBaseUrl = report.data.checks.health?.data?.upstreamBaseUrl
|
|
305
|
+
if (upstreamBaseUrl) {
|
|
306
|
+
try {
|
|
307
|
+
const response = await fetch(`${upstreamBaseUrl}/v1/article/search`, {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
headers: { 'content-type': 'application/json' },
|
|
310
|
+
body: '{}',
|
|
311
|
+
})
|
|
312
|
+
report.data.checks.upstreamReachability = {
|
|
313
|
+
ok: true,
|
|
314
|
+
status: response.status,
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
report.data.checks.upstreamReachability = {
|
|
318
|
+
ok: false,
|
|
319
|
+
message: error instanceof Error ? error.message : String(error),
|
|
320
|
+
cause:
|
|
321
|
+
error && typeof error === 'object' && 'cause' in error && error.cause instanceof Error
|
|
322
|
+
? error.cause.message
|
|
323
|
+
: '',
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
report.data.checks.upstreamReachability = {
|
|
328
|
+
ok: false,
|
|
329
|
+
message: 'upstream_not_configured',
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
printJson(report)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function main() {
|
|
337
|
+
const [command, subCommand, ...restArgs] = process.argv.slice(2)
|
|
338
|
+
|
|
339
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
340
|
+
printHelp()
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (command === 'agent' && subCommand === 'status') {
|
|
345
|
+
const payload = await request('/health')
|
|
346
|
+
if (payload) printJson(payload)
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (command === 'agent' && subCommand === 'start') {
|
|
351
|
+
await startAgent()
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (command === 'login' || (command === 'auth' && subCommand === 'login')) {
|
|
356
|
+
await runBrowserLogin()
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (command === 'status') {
|
|
361
|
+
const payload = await requestStatus()
|
|
362
|
+
if (!payload) {
|
|
363
|
+
console.log(`\n❌ 状态异常 [错误代码: 50010]`)
|
|
364
|
+
console.log(`💡 提示: ${getHint('local_agent_unavailable')}\n`)
|
|
365
|
+
process.exitCode = 1
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (payload.code !== 0) {
|
|
370
|
+
const hint = getHint(payload.msg ?? payload.message)
|
|
371
|
+
console.log(`\n❌ 状态异常 [错误代码: ${payload.code}]`)
|
|
372
|
+
if (hint) console.log(`💡 提示: ${hint}`)
|
|
373
|
+
console.log()
|
|
374
|
+
process.exitCode = 1
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
console.log(`\n🟢 本地助手运行状态:正常`)
|
|
379
|
+
console.log(`服务标识:${payload.data.service}`)
|
|
380
|
+
console.log(`登录状态:${payload.data.loggedIn ? '✅ 已登录' : '❌ 未登录'}`)
|
|
381
|
+
if (payload.data.loggedIn && payload.data.user) {
|
|
382
|
+
console.log(`当前账号:${payload.data.user.nickname || payload.data.user.username}`)
|
|
383
|
+
}
|
|
384
|
+
console.log()
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (command === 'health') {
|
|
389
|
+
const payload = await request('/health')
|
|
390
|
+
if (payload) printJson(payload)
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (command === 'doctor' || command === 'diag') {
|
|
395
|
+
await runDoctor()
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (command === 'whoami') {
|
|
400
|
+
const payload = await request('/auth/whoami')
|
|
401
|
+
if (payload && payload.data && payload.data.user) {
|
|
402
|
+
const user = payload.data.user
|
|
403
|
+
console.log(`\n👤 当前登录用户信息:`)
|
|
404
|
+
console.log(`-----------------------------------`)
|
|
405
|
+
console.log(`账号名:${user.username}`)
|
|
406
|
+
console.log(`昵 称:${user.nickname}`)
|
|
407
|
+
console.log(`机 构:${user.institution || '无'}`)
|
|
408
|
+
console.log(`角 色:${user.role === 'admin' ? '管理员' : '普通用户'}`)
|
|
409
|
+
if (payload.data.expiresAt) {
|
|
410
|
+
console.log(`过 期:${new Date(payload.data.expiresAt).toLocaleString('zh-CN')}`)
|
|
411
|
+
}
|
|
412
|
+
console.log(`-----------------------------------\n`)
|
|
413
|
+
}
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (command === 'list') {
|
|
418
|
+
if (!subCommand) {
|
|
419
|
+
const payload = await request('/commands/list')
|
|
420
|
+
if (payload) printJson(payload)
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (subCommand === 'datacenter' || subCommand === 'fengxian') {
|
|
425
|
+
const auth = await requestStatus()
|
|
426
|
+
if (!auth) return
|
|
427
|
+
if (!auth?.data?.loggedIn) {
|
|
428
|
+
printError(ErrorCode.AUTH_REQUIRED, 'auth_required', {
|
|
429
|
+
hint: 'Please sign in to Lingrui in your browser first.',
|
|
430
|
+
})
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const flags = parseFlags(restArgs)
|
|
435
|
+
printError(ErrorCode.NOT_IMPLEMENTED, 'not_implemented', {
|
|
436
|
+
command: `list ${subCommand}`,
|
|
437
|
+
params: flags,
|
|
438
|
+
note: 'Business API is not connected yet.',
|
|
439
|
+
})
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
printError(ErrorCode.INVALID_PARAMS, 'invalid_params', {
|
|
444
|
+
command: 'list',
|
|
445
|
+
supported: ['datacenter', 'fengxian'],
|
|
446
|
+
})
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (command === 'auth' && subCommand === 'logout') {
|
|
451
|
+
const payload = await request('/auth/logout', { method: 'POST' })
|
|
452
|
+
if (payload) printJson(payload)
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (command === 'logout') {
|
|
457
|
+
const payload = await request('/auth/logout', { method: 'POST' })
|
|
458
|
+
if (payload) {
|
|
459
|
+
console.log(`\n👋 已成功退出登录!\n`)
|
|
460
|
+
}
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (command === 'search') {
|
|
465
|
+
const auth = await requestStatus()
|
|
466
|
+
if (!auth) return
|
|
467
|
+
if (!auth?.data?.loggedIn) {
|
|
468
|
+
printError(ErrorCode.AUTH_REQUIRED, 'auth_required', {
|
|
469
|
+
hint: 'Please sign in to Lingrui in your browser first.',
|
|
470
|
+
})
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const legacyScene = normalizeSearchScene(subCommand)
|
|
475
|
+
const isLegacyScene = ['data-center', 'warning-center', 'website-stats'].includes(legacyScene)
|
|
476
|
+
const searchArgs = isLegacyScene ? restArgs : [subCommand, ...restArgs].filter(Boolean)
|
|
477
|
+
const { flags, terms } = parseSearchArgs(searchArgs)
|
|
478
|
+
|
|
479
|
+
if (isLegacyScene) {
|
|
480
|
+
if (legacyScene === 'warning-center') flags.center = flags.center ?? 'warning'
|
|
481
|
+
if (legacyScene === 'data-center') flags.center = flags.center ?? 'data'
|
|
482
|
+
if (legacyScene === 'website-stats') flags.stats = flags.stats ?? 'website'
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!getFlag(flags, 'q', 'keywords', 'keyword', 'kw', 'k') && terms.length > 0) {
|
|
486
|
+
flags.q = terms.join(' ')
|
|
487
|
+
}
|
|
488
|
+
const body = buildSearchBody('data-center', flags)
|
|
489
|
+
const payload = await request('/api/search', {
|
|
490
|
+
method: 'POST',
|
|
491
|
+
body,
|
|
492
|
+
})
|
|
493
|
+
if (!payload) return
|
|
494
|
+
|
|
495
|
+
const wantsJson = ['1', 'true', 'yes'].includes(
|
|
496
|
+
String(process.env.LINGRUI_CLI_JSON || '').toLowerCase(),
|
|
497
|
+
)
|
|
498
|
+
if (!wantsJson && payload?.data?.result?.data?.items) {
|
|
499
|
+
formatSearchList(payload)
|
|
500
|
+
} else {
|
|
501
|
+
printJson(payload)
|
|
502
|
+
}
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
console.log(`\n❌ 未知命令: ${command}`)
|
|
507
|
+
console.log(`💡 输入 'blynkai help' 查看支持的命令列表。\n`)
|
|
508
|
+
process.exitCode = 1
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function startAgent() {
|
|
512
|
+
try {
|
|
513
|
+
const baseUrl = await getBaseUrl('/api/search')
|
|
514
|
+
if (await hasEndpoint(baseUrl, '/api/search')) {
|
|
515
|
+
const payload = await tryRequest(baseUrl, '/health')
|
|
516
|
+
printJson({
|
|
517
|
+
code: 0,
|
|
518
|
+
msg: 'ok',
|
|
519
|
+
data: {
|
|
520
|
+
status: 'already_running',
|
|
521
|
+
health: payload.data,
|
|
522
|
+
baseUrl,
|
|
523
|
+
},
|
|
524
|
+
})
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
} catch {
|
|
528
|
+
// Start a fresh local agent below.
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
await mkdir(runtimeDir, { recursive: true })
|
|
532
|
+
const logFd = openSync(logFile, 'a')
|
|
533
|
+
const child = spawn(process.execPath, [serverEntry], {
|
|
534
|
+
detached: true,
|
|
535
|
+
stdio: ['ignore', logFd, logFd],
|
|
536
|
+
env: process.env,
|
|
537
|
+
})
|
|
538
|
+
child.unref()
|
|
539
|
+
|
|
540
|
+
resolvedBaseUrl = null
|
|
541
|
+
printJson({
|
|
542
|
+
code: 0,
|
|
543
|
+
msg: 'ok',
|
|
544
|
+
data: {
|
|
545
|
+
status: 'started',
|
|
546
|
+
baseUrl: await waitForBaseUrl('/api/search'),
|
|
547
|
+
pid: child.pid,
|
|
548
|
+
pidFile,
|
|
549
|
+
logFile,
|
|
550
|
+
},
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
main()
|