@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,201 @@
|
|
|
1
|
+
import { ErrorCode } from '../core/errors.mjs'
|
|
2
|
+
import { fail } from '../core/response.mjs'
|
|
3
|
+
|
|
4
|
+
export const SEARCH_SCENE_CONFIG = {
|
|
5
|
+
'data-center': {
|
|
6
|
+
upstreamPath: '/v1/article/search',
|
|
7
|
+
timeKind: 'publish',
|
|
8
|
+
defaultSortField: 'publish_time',
|
|
9
|
+
},
|
|
10
|
+
'warning-center': {
|
|
11
|
+
upstreamPath: '/v1/index',
|
|
12
|
+
timeKind: 'push',
|
|
13
|
+
defaultSortField: 'push_time',
|
|
14
|
+
},
|
|
15
|
+
'website-stats': {
|
|
16
|
+
upstreamPath: '/v1/article/website',
|
|
17
|
+
timeKind: 'auto',
|
|
18
|
+
defaultSortField: 'publish_time',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeCenter(value, scene) {
|
|
23
|
+
if (value === 'warning' || scene === 'warning-center') return 'warning'
|
|
24
|
+
return 'data'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveSearchConfig(body) {
|
|
28
|
+
const center = normalizeCenter(body.center, body.scene)
|
|
29
|
+
const stats = body.output?.website_stats || body.output?.stats === 'website' ? 'website' : ''
|
|
30
|
+
const scene =
|
|
31
|
+
stats === 'website' ? 'website-stats' : center === 'warning' ? 'warning-center' : 'data-center'
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
center,
|
|
35
|
+
stats,
|
|
36
|
+
scene,
|
|
37
|
+
config: {
|
|
38
|
+
...SEARCH_SCENE_CONFIG[scene],
|
|
39
|
+
timeKind: center === 'warning' ? 'push' : 'publish',
|
|
40
|
+
defaultSortField: center === 'warning' ? 'push_time' : 'publish_time',
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isPresent(value) {
|
|
46
|
+
if (value === undefined || value === null) return false
|
|
47
|
+
if (Array.isArray(value)) return value.length > 0
|
|
48
|
+
if (typeof value === 'string') return value.trim() !== ''
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cleanObject(value) {
|
|
53
|
+
return Object.fromEntries(
|
|
54
|
+
Object.entries(value).filter(([, item]) => {
|
|
55
|
+
if (item === undefined) return false
|
|
56
|
+
if (Array.isArray(item)) return item.length > 0
|
|
57
|
+
return true
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatDateTime(date) {
|
|
63
|
+
const pad = (value) => String(value).padStart(2, '0')
|
|
64
|
+
return [
|
|
65
|
+
date.getFullYear(),
|
|
66
|
+
pad(date.getMonth() + 1),
|
|
67
|
+
pad(date.getDate()),
|
|
68
|
+
].join('-')
|
|
69
|
+
+ 'T'
|
|
70
|
+
+ [pad(date.getHours()), pad(date.getMinutes()), pad(date.getSeconds())].join(':')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeDateTime(value) {
|
|
74
|
+
if (!isPresent(value)) return ''
|
|
75
|
+
if (typeof value !== 'string') return String(value)
|
|
76
|
+
const trimmed = value.trim()
|
|
77
|
+
if (!trimmed) return ''
|
|
78
|
+
|
|
79
|
+
const noTimezone = trimmed.replace(/Z$/, '').replace(/[+-]\d{2}:\d{2}$/, '')
|
|
80
|
+
const normalized = noTimezone.replace(/\.\d+$/, '')
|
|
81
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(normalized)) return normalized
|
|
82
|
+
|
|
83
|
+
const parsed = new Date(trimmed)
|
|
84
|
+
if (Number.isNaN(parsed.getTime())) return normalized
|
|
85
|
+
return formatDateTime(parsed)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function defaultTimeRange() {
|
|
89
|
+
const end = new Date()
|
|
90
|
+
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000)
|
|
91
|
+
return {
|
|
92
|
+
start: formatDateTime(start),
|
|
93
|
+
end: formatDateTime(end),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveTimeKind(scene, body) {
|
|
98
|
+
const config = SEARCH_SCENE_CONFIG[scene]
|
|
99
|
+
if (!config) return 'publish'
|
|
100
|
+
if (config.timeKind !== 'auto') return config.timeKind
|
|
101
|
+
|
|
102
|
+
if (isPresent(body.time?.push_start) || isPresent(body.time?.push_end)) return 'push'
|
|
103
|
+
return 'publish'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function normalizeSearchPayload(body) {
|
|
107
|
+
const { center, stats, scene, config } = resolveSearchConfig(body)
|
|
108
|
+
if (!config) {
|
|
109
|
+
return {
|
|
110
|
+
error: fail(ErrorCode.INVALID_PARAMS, {
|
|
111
|
+
field: 'center',
|
|
112
|
+
supported: ['data', 'warning'],
|
|
113
|
+
}),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const timeKind = config.timeKind || resolveTimeKind(scene, body)
|
|
118
|
+
const time = body.time ?? {}
|
|
119
|
+
const query = body.query ?? {}
|
|
120
|
+
const filters = body.filters ?? {}
|
|
121
|
+
const page = body.page ?? {}
|
|
122
|
+
const sort = body.sort ?? {}
|
|
123
|
+
const fallbackTime = defaultTimeRange()
|
|
124
|
+
const keywords = query.keywords ?? ''
|
|
125
|
+
const hasKeywords = typeof keywords === 'string' && keywords.trim() !== ''
|
|
126
|
+
const searchMode = query.search_mode ?? 'keyword'
|
|
127
|
+
const defaultSortField =
|
|
128
|
+
searchMode === 'keyword' && hasKeywords ? '_score' : config.defaultSortField
|
|
129
|
+
|
|
130
|
+
const publishStart =
|
|
131
|
+
timeKind === 'publish'
|
|
132
|
+
? normalizeDateTime(time.publish_start || time.start || fallbackTime.start)
|
|
133
|
+
: normalizeDateTime(time.publish_start || '')
|
|
134
|
+
const publishEnd =
|
|
135
|
+
timeKind === 'publish'
|
|
136
|
+
? normalizeDateTime(time.publish_end || time.end || fallbackTime.end)
|
|
137
|
+
: normalizeDateTime(time.publish_end || '')
|
|
138
|
+
const pushStart =
|
|
139
|
+
timeKind === 'push'
|
|
140
|
+
? normalizeDateTime(time.push_start || time.start || fallbackTime.start)
|
|
141
|
+
: normalizeDateTime(time.push_start || '')
|
|
142
|
+
const pushEnd =
|
|
143
|
+
timeKind === 'push'
|
|
144
|
+
? normalizeDateTime(time.push_end || time.end || fallbackTime.end)
|
|
145
|
+
: normalizeDateTime(time.push_end || '')
|
|
146
|
+
|
|
147
|
+
const payload = cleanObject({
|
|
148
|
+
publish_time_start: publishStart,
|
|
149
|
+
publish_time_end: publishEnd,
|
|
150
|
+
push_time_start: pushStart,
|
|
151
|
+
push_time_end: pushEnd,
|
|
152
|
+
keywords,
|
|
153
|
+
article_categories: filters.article_categories ?? [],
|
|
154
|
+
media_channels: filters.media_channels ?? [],
|
|
155
|
+
website_names: filters.website_names ?? [],
|
|
156
|
+
sentiment: filters.sentiment ?? [],
|
|
157
|
+
risk_levels: filters.risk_levels ?? [],
|
|
158
|
+
media_tags: filters.media_tags ?? [],
|
|
159
|
+
search_fields: query.search_fields ?? [],
|
|
160
|
+
article_type: filters.article_type ?? [],
|
|
161
|
+
article_id_list: filters.article_id_list ?? [],
|
|
162
|
+
article_status: filters.article_status ?? '',
|
|
163
|
+
status: filters.status ?? '',
|
|
164
|
+
is_valid:
|
|
165
|
+
center === 'warning'
|
|
166
|
+
? filters.is_valid ?? ''
|
|
167
|
+
: filters.is_valid === undefined
|
|
168
|
+
? 'Valid'
|
|
169
|
+
: filters.is_valid,
|
|
170
|
+
third_party: filters.third_party ?? [],
|
|
171
|
+
third_party_id: filters.third_party_id ?? null,
|
|
172
|
+
sort_field: sort.field ?? defaultSortField,
|
|
173
|
+
sort_order: sort.order ?? 'desc',
|
|
174
|
+
keywords_must_include: query.keywords_must_include ?? [],
|
|
175
|
+
keywords_should_include: query.keywords_should_include ?? [],
|
|
176
|
+
keywords_must_exclude: query.keywords_must_exclude ?? [],
|
|
177
|
+
page: page.page ?? 1,
|
|
178
|
+
size: page.size ?? 10,
|
|
179
|
+
search_mode: searchMode,
|
|
180
|
+
semantic_query: query.semantic_query ?? null,
|
|
181
|
+
semantic_top_n: query.semantic_top_n ?? null,
|
|
182
|
+
collapse_field: filters.collapse_field ?? '',
|
|
183
|
+
review_status:
|
|
184
|
+
center === 'warning' ? filters.review_status ?? 'pushed' : filters.review_status ?? null,
|
|
185
|
+
reviewer: filters.reviewer ?? null,
|
|
186
|
+
is_similar_collapse: Boolean(filters.is_similar_collapse),
|
|
187
|
+
topic_id: filters.topic_id,
|
|
188
|
+
warning_cluster_id: filters.warning_cluster_id,
|
|
189
|
+
exclude_ids: filters.exclude_ids ?? [],
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
scene,
|
|
194
|
+
center,
|
|
195
|
+
stats,
|
|
196
|
+
upstreamPath: config.upstreamPath,
|
|
197
|
+
timeKind,
|
|
198
|
+
payload,
|
|
199
|
+
dryRun: Boolean(body.output?.dry_run),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createServer } from 'node:http'
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { resolveUpstreamBaseUrl } from '../core/config.mjs'
|
|
6
|
+
import { ErrorCode } from '../core/errors.mjs'
|
|
7
|
+
import { fail, ok, sendJson } from '../core/response.mjs'
|
|
8
|
+
import { logFile, pidFile, runtimeDir } from '../core/runtime-paths.mjs'
|
|
9
|
+
import { getSessionState, revokeSession, syncBrowserSession } from '../core/session-store.mjs'
|
|
10
|
+
import { availableCommands } from '../data/commands.mjs'
|
|
11
|
+
import { normalizeSearchPayload } from '../search/normalize-payload.mjs'
|
|
12
|
+
|
|
13
|
+
const host = '127.0.0.1'
|
|
14
|
+
const preferredPort = Number(process.env.LINGRUI_AGENT_PORT ?? 38100)
|
|
15
|
+
let activePort = preferredPort
|
|
16
|
+
const upstreamBaseUrl = resolveUpstreamBaseUrl()
|
|
17
|
+
const upstreamTimeoutMs = Number(process.env.LINGRUI_UPSTREAM_TIMEOUT_MS ?? 15000)
|
|
18
|
+
|
|
19
|
+
function isLoopback(req) {
|
|
20
|
+
const address = req.socket.remoteAddress
|
|
21
|
+
return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function requireActiveSession() {
|
|
25
|
+
const { state, session } = await getSessionState()
|
|
26
|
+
if (state === 'active') return { authorized: true, session }
|
|
27
|
+
if (state === 'revoked') return { authorized: false, code: ErrorCode.SESSION_REVOKED }
|
|
28
|
+
if (state === 'expired') return { authorized: false, code: ErrorCode.SESSION_EXPIRED }
|
|
29
|
+
return { authorized: false, code: ErrorCode.AUTH_REQUIRED }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readJson(req) {
|
|
33
|
+
const chunks = []
|
|
34
|
+
for await (const chunk of req) {
|
|
35
|
+
chunks.push(chunk)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!chunks.length) return {}
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function callUpstream(path, payload, auth) {
|
|
47
|
+
if (!upstreamBaseUrl) {
|
|
48
|
+
return {
|
|
49
|
+
proxied: false,
|
|
50
|
+
reason: 'LINGRUI_UPSTREAM_BASE_URL_not_configured',
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const token = auth.session?.token?.value
|
|
55
|
+
if (!token) {
|
|
56
|
+
return {
|
|
57
|
+
proxied: false,
|
|
58
|
+
reason: 'browser_token_not_available_in_memory',
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const controller = new AbortController()
|
|
63
|
+
const timeout = setTimeout(() => controller.abort(), upstreamTimeoutMs)
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(`${upstreamBaseUrl}${path}`, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'content-type': 'application/json',
|
|
70
|
+
Authorization: `bearer ${token}`,
|
|
71
|
+
},
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
body: JSON.stringify(payload),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const data = await response.json()
|
|
77
|
+
return {
|
|
78
|
+
proxied: true,
|
|
79
|
+
status: response.status,
|
|
80
|
+
data,
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const causeMessage =
|
|
84
|
+
error && typeof error === 'object' && 'cause' in error && error.cause instanceof Error
|
|
85
|
+
? error.cause.message
|
|
86
|
+
: ''
|
|
87
|
+
return {
|
|
88
|
+
proxied: false,
|
|
89
|
+
reason: error?.name === 'AbortError' ? 'upstream_request_timeout' : 'upstream_request_failed',
|
|
90
|
+
message: error instanceof Error ? error.message : String(error),
|
|
91
|
+
...(causeMessage ? { cause: causeMessage } : {}),
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timeout)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function route(req, res) {
|
|
99
|
+
if (req.method === 'OPTIONS') {
|
|
100
|
+
return sendJson(res, 200, ok())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!isLoopback(req)) {
|
|
104
|
+
return sendJson(res, 403, fail(ErrorCode.LOOPBACK_ONLY))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const url = new URL(req.url ?? '/', `http://${host}:${activePort}`)
|
|
108
|
+
const key = `${req.method} ${url.pathname}`
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
if (key === 'GET /health') {
|
|
112
|
+
const { state } = await getSessionState()
|
|
113
|
+
return sendJson(
|
|
114
|
+
res,
|
|
115
|
+
200,
|
|
116
|
+
ok({
|
|
117
|
+
service: 'lingrui-local-agent',
|
|
118
|
+
status: 'online',
|
|
119
|
+
authState: state,
|
|
120
|
+
version: '0.1.0',
|
|
121
|
+
port: activePort,
|
|
122
|
+
upstreamConfigured: Boolean(upstreamBaseUrl),
|
|
123
|
+
upstreamBaseUrl: upstreamBaseUrl || null,
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (key === 'GET /status') {
|
|
129
|
+
const { state, session } = await getSessionState()
|
|
130
|
+
return sendJson(
|
|
131
|
+
res,
|
|
132
|
+
200,
|
|
133
|
+
ok({
|
|
134
|
+
service: 'lingrui-cli',
|
|
135
|
+
status: 'online',
|
|
136
|
+
loggedIn: state === 'active',
|
|
137
|
+
authState: state,
|
|
138
|
+
user: state === 'active' ? session?.user ?? null : null,
|
|
139
|
+
port: activePort,
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (key === 'POST /auth/sync' || key === 'POST /auth/reconnect') {
|
|
145
|
+
const payload = await readJson(req)
|
|
146
|
+
if (!payload) {
|
|
147
|
+
return sendJson(res, 400, fail(ErrorCode.INVALID_PARAMS, { field: 'body' }))
|
|
148
|
+
}
|
|
149
|
+
if (!payload?.token) {
|
|
150
|
+
return sendJson(res, 400, fail(ErrorCode.INVALID_PARAMS, { field: 'token' }))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const session = await syncBrowserSession(payload)
|
|
154
|
+
return sendJson(
|
|
155
|
+
res,
|
|
156
|
+
200,
|
|
157
|
+
ok({
|
|
158
|
+
sessionId: session.sessionId,
|
|
159
|
+
expiresAt: session.expiresAt,
|
|
160
|
+
user: session.user,
|
|
161
|
+
scopes: session.scopes,
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (key === 'GET /auth/whoami') {
|
|
167
|
+
const auth = await requireActiveSession()
|
|
168
|
+
if (!auth.authorized) return sendJson(res, 401, fail(auth.code))
|
|
169
|
+
return sendJson(
|
|
170
|
+
res,
|
|
171
|
+
200,
|
|
172
|
+
ok({
|
|
173
|
+
user: auth.session.user,
|
|
174
|
+
scopes: auth.session.scopes,
|
|
175
|
+
expiresAt: auth.session.expiresAt,
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (key === 'GET /commands/list') {
|
|
181
|
+
const auth = await requireActiveSession()
|
|
182
|
+
if (!auth.authorized) return sendJson(res, 401, fail(auth.code))
|
|
183
|
+
return sendJson(
|
|
184
|
+
res,
|
|
185
|
+
200,
|
|
186
|
+
ok({
|
|
187
|
+
items: availableCommands,
|
|
188
|
+
total: availableCommands.length,
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (key === 'POST /auth/logout') {
|
|
194
|
+
await revokeSession()
|
|
195
|
+
console.log('[auth] browser signed out, local session revoked')
|
|
196
|
+
return sendJson(res, 200, ok({ status: 'revoked' }))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (key === 'POST /skills/check') {
|
|
200
|
+
const auth = await requireActiveSession()
|
|
201
|
+
if (!auth.authorized) return sendJson(res, 401, fail(auth.code))
|
|
202
|
+
return sendJson(
|
|
203
|
+
res,
|
|
204
|
+
200,
|
|
205
|
+
ok({
|
|
206
|
+
allowed: true,
|
|
207
|
+
status: 'logged_in',
|
|
208
|
+
user: auth.session.user,
|
|
209
|
+
}),
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (key === 'POST /api/search') {
|
|
214
|
+
const auth = await requireActiveSession()
|
|
215
|
+
if (!auth.authorized) return sendJson(res, 401, fail(auth.code))
|
|
216
|
+
|
|
217
|
+
const body = await readJson(req)
|
|
218
|
+
const normalized = normalizeSearchPayload(body)
|
|
219
|
+
if (normalized.error) return sendJson(res, 400, normalized.error)
|
|
220
|
+
|
|
221
|
+
if (!normalized.dryRun) {
|
|
222
|
+
const upstream = await callUpstream(normalized.upstreamPath, normalized.payload, auth)
|
|
223
|
+
if (upstream.proxied) {
|
|
224
|
+
return sendJson(
|
|
225
|
+
res,
|
|
226
|
+
200,
|
|
227
|
+
ok({
|
|
228
|
+
scene: normalized.scene,
|
|
229
|
+
center: normalized.center,
|
|
230
|
+
stats: normalized.stats,
|
|
231
|
+
upstream_path: normalized.upstreamPath,
|
|
232
|
+
upstream_status: upstream.status,
|
|
233
|
+
result: upstream.data,
|
|
234
|
+
}),
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return sendJson(
|
|
239
|
+
res,
|
|
240
|
+
200,
|
|
241
|
+
ok({
|
|
242
|
+
scene: normalized.scene,
|
|
243
|
+
center: normalized.center,
|
|
244
|
+
stats: normalized.stats,
|
|
245
|
+
upstream_path: normalized.upstreamPath,
|
|
246
|
+
proxied: false,
|
|
247
|
+
reason: upstream.reason,
|
|
248
|
+
...(upstream.status ? { upstream_status: upstream.status } : {}),
|
|
249
|
+
...(upstream.message ? { message: upstream.message } : {}),
|
|
250
|
+
...(upstream.cause ? { cause: upstream.cause } : {}),
|
|
251
|
+
payload: normalized.payload,
|
|
252
|
+
}),
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return sendJson(
|
|
257
|
+
res,
|
|
258
|
+
200,
|
|
259
|
+
ok({
|
|
260
|
+
scene: normalized.scene,
|
|
261
|
+
center: normalized.center,
|
|
262
|
+
stats: normalized.stats,
|
|
263
|
+
upstream_path: normalized.upstreamPath,
|
|
264
|
+
dry_run: true,
|
|
265
|
+
payload: normalized.payload,
|
|
266
|
+
}),
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return sendJson(res, 404, fail(ErrorCode.NOT_FOUND, { path: url.pathname }))
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return sendJson(
|
|
273
|
+
res,
|
|
274
|
+
500,
|
|
275
|
+
fail(ErrorCode.INTERNAL_ERROR, {
|
|
276
|
+
message: error instanceof Error ? error.message : 'unknown error',
|
|
277
|
+
}),
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const server = createServer(route)
|
|
283
|
+
|
|
284
|
+
function listenOnPort(portToTry) {
|
|
285
|
+
server.listen(portToTry, host)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
server.on('error', (error) => {
|
|
289
|
+
if (error?.code === 'EADDRINUSE' && activePort < preferredPort + 20) {
|
|
290
|
+
const nextPort = activePort + 1
|
|
291
|
+
console.warn(`Port ${activePort} is in use, trying ${nextPort}...`)
|
|
292
|
+
activePort = nextPort
|
|
293
|
+
listenOnPort(activePort)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
throw error
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
server.on('listening', async () => {
|
|
301
|
+
await mkdir(runtimeDir, { recursive: true })
|
|
302
|
+
await writeFile(pidFile, String(process.pid), 'utf8')
|
|
303
|
+
console.log(`Lingrui local agent is running at http://${host}:${activePort}`)
|
|
304
|
+
console.log(`Runtime dir: ${runtimeDir}`)
|
|
305
|
+
console.log(`Log file: ${logFile}`)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
listenOnPort(activePort)
|