@realsee/vr-cli 0.1.5 → 0.2.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 +47 -24
- package/agent-assets/AGENTS.md +7 -5
- package/dist/cli.mjs +5 -5
- package/package.json +1 -1
- package/skills/realsee-vr-app/SKILL.md +1 -1
- package/skills/realsee-vr-app/references/dnalogel-openapi-recipes.md +90 -26
- package/skills/realsee-vr-app/references/five-runtime.md +2 -2
- package/skills/realsee-vr-app/scripts/verify-project.mjs +186 -15
- package/templates/base/.env.example +2 -2
- package/templates/base/.gitignore.template +2 -0
- package/templates/base/.prettierignore +2 -0
- package/templates/base/README.md +32 -6
- package/templates/base/deploy.json +11 -0
- package/templates/base/eslint.config.mjs +2 -0
- package/templates/base/package-lock.json +11 -0
- package/templates/base/package.json +4 -1
- package/templates/base/scripts/package-vibe-release.mjs +569 -0
- package/templates/base/scripts/pairing.mjs +564 -0
- package/templates/base/scripts/pairing.test.mjs +252 -0
- package/templates/base/scripts/project-identity.mjs +160 -0
- package/templates/base/scripts/project-identity.test.mjs +119 -0
- package/templates/base/scripts/setup-config.mjs +96 -23
- package/templates/base/scripts/setup-config.test.mjs +49 -9
- package/templates/base/scripts/setup.mjs +161 -7
- package/templates/base/server/app.ts +1 -1
- package/templates/base/server/config.ts +5 -4
- package/templates/base/server/types.ts +1 -1
- package/templates/base/server/work-route.ts +14 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import { chmod, lstat, open, rename, rm } from 'node:fs/promises'
|
|
2
|
+
import { chmod, lstat, mkdir, open, rename, rm } from 'node:fs/promises'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { spawn } from 'node:child_process'
|
|
@@ -8,6 +8,8 @@ export const SETUP_PAYLOAD_PREFIX = 'realsee-setup:v1:'
|
|
|
8
8
|
export const DEFAULT_GATEWAY_ENDPOINT = 'https://app-gateway.realsee.cn'
|
|
9
9
|
|
|
10
10
|
const RESOURCE_CODE_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
|
|
11
|
+
const GATEWAY_ENDPOINT_PATTERN =
|
|
12
|
+
/^https:\/\/[A-Za-z0-9.-]+(?::\d+)?(?:\/[A-Za-z0-9._~%/-]*)?$/u
|
|
11
13
|
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/
|
|
12
14
|
const MAX_PAYLOAD_LENGTH = 8_192
|
|
13
15
|
const MAX_CREDENTIAL_LENGTH = 1_024
|
|
@@ -16,7 +18,11 @@ function setupError(message) {
|
|
|
16
18
|
return new Error(`配置包无效:${message}`)
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
function
|
|
21
|
+
function credentialError(message) {
|
|
22
|
+
return new Error(`网关凭证无效:${message}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function exactString(value, label, makeError = setupError) {
|
|
20
26
|
if (
|
|
21
27
|
typeof value !== 'string' ||
|
|
22
28
|
value.length === 0 ||
|
|
@@ -24,11 +30,44 @@ function exactString(value, label) {
|
|
|
24
30
|
value !== value.trim() ||
|
|
25
31
|
/[\0\r\n]/u.test(value)
|
|
26
32
|
) {
|
|
27
|
-
throw
|
|
33
|
+
throw makeError(`${label} 缺失或格式不正确。`)
|
|
28
34
|
}
|
|
29
35
|
return value
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
// Normalizes credentials resolved outside the pasted payload (environment
|
|
39
|
+
// variables and the pairing protocol) into the exact shape the writer renders.
|
|
40
|
+
export function normalizeGatewayCredentials({
|
|
41
|
+
appKey,
|
|
42
|
+
appSecret,
|
|
43
|
+
resourceCode,
|
|
44
|
+
gatewayEndpoint,
|
|
45
|
+
}) {
|
|
46
|
+
const config = {
|
|
47
|
+
appKey: exactString(appKey, 'AK', credentialError),
|
|
48
|
+
appSecret: exactString(appSecret, 'SK', credentialError),
|
|
49
|
+
}
|
|
50
|
+
if (resourceCode !== undefined && resourceCode !== '') {
|
|
51
|
+
const value = exactString(resourceCode, 'resourceCode', credentialError)
|
|
52
|
+
if (!RESOURCE_CODE_PATTERN.test(value)) {
|
|
53
|
+
throw credentialError('resourceCode 格式不正确。')
|
|
54
|
+
}
|
|
55
|
+
config.resourceCode = value
|
|
56
|
+
}
|
|
57
|
+
if (gatewayEndpoint !== undefined && gatewayEndpoint !== '') {
|
|
58
|
+
const value = exactString(
|
|
59
|
+
gatewayEndpoint,
|
|
60
|
+
'gatewayEndpoint',
|
|
61
|
+
credentialError,
|
|
62
|
+
)
|
|
63
|
+
if (!GATEWAY_ENDPOINT_PATTERN.test(value)) {
|
|
64
|
+
throw credentialError('gatewayEndpoint 必须是 HTTPS URL。')
|
|
65
|
+
}
|
|
66
|
+
config.gatewayEndpoint = value
|
|
67
|
+
}
|
|
68
|
+
return Object.freeze(config)
|
|
69
|
+
}
|
|
70
|
+
|
|
32
71
|
export function decodeSetupPayload(input) {
|
|
33
72
|
const value = typeof input === 'string' ? input.trim() : ''
|
|
34
73
|
if (!value.startsWith(SETUP_PAYLOAD_PREFIX)) {
|
|
@@ -66,31 +105,49 @@ export function decodeSetupPayload(input) {
|
|
|
66
105
|
throw setupError('内容结构不正确。')
|
|
67
106
|
}
|
|
68
107
|
const keys = Object.keys(parsed).sort()
|
|
69
|
-
|
|
108
|
+
const keyList = keys.join(',')
|
|
109
|
+
if (
|
|
110
|
+
keyList !== 'appKey,appSecret' &&
|
|
111
|
+
keyList !== 'appKey,appSecret,resourceCode'
|
|
112
|
+
) {
|
|
70
113
|
throw setupError('字段集合不正确。')
|
|
71
114
|
}
|
|
72
115
|
|
|
73
116
|
const appKey = exactString(parsed.appKey, 'AK')
|
|
74
117
|
const appSecret = exactString(parsed.appSecret, 'SK')
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
118
|
+
let resourceCode
|
|
119
|
+
if (parsed.resourceCode !== undefined && parsed.resourceCode !== '') {
|
|
120
|
+
resourceCode = exactString(parsed.resourceCode, 'resourceCode')
|
|
121
|
+
if (!RESOURCE_CODE_PATTERN.test(resourceCode)) {
|
|
122
|
+
throw setupError('resourceCode 格式不正确。')
|
|
123
|
+
}
|
|
78
124
|
}
|
|
79
125
|
|
|
80
|
-
return Object.freeze({
|
|
126
|
+
return Object.freeze({
|
|
127
|
+
appKey,
|
|
128
|
+
appSecret,
|
|
129
|
+
...(resourceCode === undefined ? {} : { resourceCode }),
|
|
130
|
+
})
|
|
81
131
|
}
|
|
82
132
|
|
|
83
133
|
function quoteDotenvValue(value) {
|
|
84
134
|
return JSON.stringify(value)
|
|
85
135
|
}
|
|
86
136
|
|
|
87
|
-
export function renderLocalEnv({
|
|
137
|
+
export function renderLocalEnv({
|
|
138
|
+
appKey,
|
|
139
|
+
appSecret,
|
|
140
|
+
resourceCode,
|
|
141
|
+
gatewayEndpoint,
|
|
142
|
+
}) {
|
|
88
143
|
return [
|
|
89
144
|
'# Generated by npm run setup. Keep this server-only file out of version control.',
|
|
90
|
-
`REALSEE_GATEWAY_ENDPOINT=${DEFAULT_GATEWAY_ENDPOINT}`,
|
|
145
|
+
`REALSEE_GATEWAY_ENDPOINT=${gatewayEndpoint ?? DEFAULT_GATEWAY_ENDPOINT}`,
|
|
91
146
|
`REALSEE_GATEWAY_APP_KEY=${quoteDotenvValue(appKey)}`,
|
|
92
147
|
`REALSEE_GATEWAY_APP_SECRET=${quoteDotenvValue(appSecret)}`,
|
|
93
|
-
|
|
148
|
+
...(resourceCode
|
|
149
|
+
? [`REALSEE_DEFAULT_RESOURCE_CODE=${quoteDotenvValue(resourceCode)}`]
|
|
150
|
+
: []),
|
|
94
151
|
'',
|
|
95
152
|
].join('\n')
|
|
96
153
|
}
|
|
@@ -144,27 +201,25 @@ async function assertSafeTarget(targetPath) {
|
|
|
144
201
|
}
|
|
145
202
|
}
|
|
146
203
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} = {},
|
|
204
|
+
// Shared atomic write chain: restrictive umask, exclusive 0600 temp file,
|
|
205
|
+
// fsync, platform permission hardening, then rename into place. Used for
|
|
206
|
+
// `.env.development` and for the 0600 pairing continuation file.
|
|
207
|
+
export async function writePrivateFile(
|
|
208
|
+
targetPath,
|
|
209
|
+
content,
|
|
210
|
+
{ platform = process.platform, env = process.env, runCommand = run } = {},
|
|
155
211
|
) {
|
|
156
|
-
|
|
157
|
-
await assertSafeTarget(targetPath)
|
|
212
|
+
await mkdir(path.dirname(targetPath), { recursive: true, mode: 0o700 })
|
|
158
213
|
const temporaryPath = path.join(
|
|
159
214
|
path.dirname(targetPath),
|
|
160
|
-
|
|
215
|
+
`.${path.basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`,
|
|
161
216
|
)
|
|
162
217
|
const previousUmask = platform === 'win32' ? undefined : process.umask(0o077)
|
|
163
218
|
|
|
164
219
|
try {
|
|
165
220
|
const handle = await open(temporaryPath, 'wx', 0o600)
|
|
166
221
|
try {
|
|
167
|
-
await handle.writeFile(
|
|
222
|
+
await handle.writeFile(content, 'utf8')
|
|
168
223
|
await handle.sync()
|
|
169
224
|
} finally {
|
|
170
225
|
await handle.close()
|
|
@@ -177,3 +232,21 @@ export async function writeLocalConfig(
|
|
|
177
232
|
await rm(temporaryPath, { force: true }).catch(() => undefined)
|
|
178
233
|
}
|
|
179
234
|
}
|
|
235
|
+
|
|
236
|
+
export async function writeLocalConfig(
|
|
237
|
+
config,
|
|
238
|
+
{
|
|
239
|
+
root = process.cwd(),
|
|
240
|
+
platform = process.platform,
|
|
241
|
+
env = process.env,
|
|
242
|
+
runCommand = run,
|
|
243
|
+
} = {},
|
|
244
|
+
) {
|
|
245
|
+
const targetPath = path.join(path.resolve(root), '.env.development')
|
|
246
|
+
await assertSafeTarget(targetPath)
|
|
247
|
+
return writePrivateFile(targetPath, renderLocalEnv(config), {
|
|
248
|
+
platform,
|
|
249
|
+
env,
|
|
250
|
+
runCommand,
|
|
251
|
+
})
|
|
252
|
+
}
|
|
@@ -16,29 +16,59 @@ const CONFIG = Object.freeze({
|
|
|
16
16
|
appSecret: '测试-secret_+/=',
|
|
17
17
|
resourceCode: 'resource-code_1',
|
|
18
18
|
})
|
|
19
|
+
const CONFIG_WITHOUT_RESOURCE = Object.freeze({
|
|
20
|
+
appKey: CONFIG.appKey,
|
|
21
|
+
appSecret: CONFIG.appSecret,
|
|
22
|
+
})
|
|
19
23
|
|
|
20
24
|
const ENCODED =
|
|
21
25
|
'eyJhcHBLZXkiOiJ0ZXN0LWFwcC1rZXkiLCJhcHBTZWNyZXQiOiLmtYvor5Utc2VjcmV0XysvPSIsInJlc291cmNlQ29kZSI6InJlc291cmNlLWNvZGVfMSJ9'
|
|
26
|
+
const encode = (value) =>
|
|
27
|
+
Buffer.from(JSON.stringify(value)).toString('base64url')
|
|
22
28
|
|
|
23
|
-
test('decodes the versioned
|
|
29
|
+
test('decodes the existing versioned payload with its optional resource fallback', () => {
|
|
24
30
|
assert.deepEqual(
|
|
25
31
|
decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${ENCODED}`),
|
|
26
32
|
CONFIG,
|
|
27
33
|
)
|
|
28
34
|
})
|
|
29
35
|
|
|
30
|
-
test('
|
|
36
|
+
test('accepts a v1 setup payload without a resource fallback', () => {
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
decodeSetupPayload(
|
|
39
|
+
`${SETUP_PAYLOAD_PREFIX}${encode(CONFIG_WITHOUT_RESOURCE)}`,
|
|
40
|
+
),
|
|
41
|
+
CONFIG_WITHOUT_RESOURCE,
|
|
42
|
+
)
|
|
43
|
+
assert.deepEqual(
|
|
44
|
+
decodeSetupPayload(
|
|
45
|
+
`${SETUP_PAYLOAD_PREFIX}${encode({ ...CONFIG_WITHOUT_RESOURCE, resourceCode: '' })}`,
|
|
46
|
+
),
|
|
47
|
+
CONFIG_WITHOUT_RESOURCE,
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('rejects non-canonical, malformed and extra-field payloads', () => {
|
|
31
52
|
assert.throws(() => decodeSetupPayload('not-a-setup-payload'))
|
|
32
53
|
assert.throws(() => decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${ENCODED}=`))
|
|
33
|
-
const withExtraField = Buffer.from(
|
|
34
|
-
JSON.stringify({ ...CONFIG, unexpected: true }),
|
|
35
|
-
).toString('base64url')
|
|
36
54
|
assert.throws(() =>
|
|
37
|
-
decodeSetupPayload(
|
|
55
|
+
decodeSetupPayload(
|
|
56
|
+
`${SETUP_PAYLOAD_PREFIX}${encode({ appKey: CONFIG.appKey })}`,
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
assert.throws(() =>
|
|
60
|
+
decodeSetupPayload(
|
|
61
|
+
`${SETUP_PAYLOAD_PREFIX}${encode({ ...CONFIG, resourceCode: 'invalid code' })}`,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
assert.throws(() =>
|
|
65
|
+
decodeSetupPayload(
|
|
66
|
+
`${SETUP_PAYLOAD_PREFIX}${encode({ ...CONFIG, unexpected: true })}`,
|
|
67
|
+
),
|
|
38
68
|
)
|
|
39
69
|
})
|
|
40
70
|
|
|
41
|
-
test('renders
|
|
71
|
+
test('renders the optional fallback only when it is configured', () => {
|
|
42
72
|
const rendered = renderLocalEnv(CONFIG)
|
|
43
73
|
assert.match(
|
|
44
74
|
rendered,
|
|
@@ -48,14 +78,24 @@ test('renders one server-only env file without printing the setup payload', () =
|
|
|
48
78
|
assert.match(rendered, /REALSEE_GATEWAY_APP_SECRET="测试-secret_\+\/="/u)
|
|
49
79
|
assert.match(rendered, /REALSEE_DEFAULT_RESOURCE_CODE="resource-code_1"/u)
|
|
50
80
|
assert.doesNotMatch(rendered, /realsee-setup:v1:/u)
|
|
81
|
+
|
|
82
|
+
const withoutResource = renderLocalEnv(CONFIG_WITHOUT_RESOURCE)
|
|
83
|
+
assert.match(withoutResource, /REALSEE_GATEWAY_APP_KEY="test-app-key"/u)
|
|
84
|
+
assert.doesNotMatch(withoutResource, /REALSEE_DEFAULT_RESOURCE_CODE/u)
|
|
51
85
|
})
|
|
52
86
|
|
|
53
87
|
test('writes atomically with restrictive POSIX permissions', async () => {
|
|
54
88
|
if (process.platform === 'win32') return
|
|
55
89
|
const root = await mkdtemp(path.join(os.tmpdir(), 'realsee-setup-'))
|
|
56
90
|
try {
|
|
57
|
-
const target = await writeLocalConfig(
|
|
58
|
-
|
|
91
|
+
const target = await writeLocalConfig(CONFIG_WITHOUT_RESOURCE, {
|
|
92
|
+
root,
|
|
93
|
+
platform: 'linux',
|
|
94
|
+
})
|
|
95
|
+
assert.equal(
|
|
96
|
+
await readFile(target, 'utf8'),
|
|
97
|
+
renderLocalEnv(CONFIG_WITHOUT_RESOURCE),
|
|
98
|
+
)
|
|
59
99
|
assert.equal((await stat(target)).mode & 0o777, 0o600)
|
|
60
100
|
} finally {
|
|
61
101
|
await rm(root, { recursive: true, force: true })
|
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url'
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
2
3
|
import { Writable } from 'node:stream'
|
|
3
4
|
import { createInterface } from 'node:readline/promises'
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
decodeSetupPayload,
|
|
7
|
+
normalizeGatewayCredentials,
|
|
8
|
+
writeLocalConfig,
|
|
9
|
+
} from './setup-config.mjs'
|
|
10
|
+
import {
|
|
11
|
+
PAIRING_EXIT_CODES,
|
|
12
|
+
maskAppKey,
|
|
13
|
+
reportPairingFailure,
|
|
14
|
+
runPairingCommand,
|
|
15
|
+
runPairingCompleteCommand,
|
|
16
|
+
} from './pairing.mjs'
|
|
17
|
+
|
|
18
|
+
const HELP = `用法:npm run setup [-- <选项>]
|
|
19
|
+
|
|
20
|
+
默认走配对:发起请求,打印验证链接与短码,阻塞轮询到终态并写入 .env.development。
|
|
21
|
+
--pair 显式选择配对路径(默认行为)。
|
|
22
|
+
--no-wait 发起后立即退出(退出码 2),稍后用 --complete 续做。
|
|
23
|
+
--complete 读取 .realsee/pairing-continuation.json,轮询到终态并写入。
|
|
24
|
+
--paste 回退路径:把 Portal 复制的单行配置包粘贴到隐藏输入。
|
|
25
|
+
--json 只输出 JSON 信封,不打印人类可读提示。
|
|
26
|
+
-h, --help 显示本帮助。
|
|
27
|
+
|
|
28
|
+
环境变量 REALSEE_GATEWAY_APP_KEY / REALSEE_GATEWAY_APP_SECRET 优先级最高,
|
|
29
|
+
设置后跳过配对,直接写入 .env.development。
|
|
30
|
+
`
|
|
5
31
|
|
|
6
32
|
class HiddenOutput extends Writable {
|
|
7
33
|
_write(_chunk, _encoding, callback) {
|
|
@@ -33,17 +59,145 @@ async function readHiddenPayload() {
|
|
|
33
59
|
}
|
|
34
60
|
}
|
|
35
61
|
|
|
36
|
-
|
|
62
|
+
async function runPasteFlow() {
|
|
37
63
|
const payload = decodeSetupPayload(await readHiddenPayload())
|
|
38
64
|
await writeLocalConfig(payload)
|
|
39
65
|
console.log('配置完成。请回到 AI 助手并回复“已完成”。')
|
|
40
66
|
}
|
|
41
67
|
|
|
68
|
+
function parseSetupArguments(argv) {
|
|
69
|
+
try {
|
|
70
|
+
return parseArgs({
|
|
71
|
+
args: argv,
|
|
72
|
+
allowPositionals: false,
|
|
73
|
+
strict: true,
|
|
74
|
+
options: {
|
|
75
|
+
pair: { type: 'boolean', default: false },
|
|
76
|
+
paste: { type: 'boolean', default: false },
|
|
77
|
+
complete: { type: 'boolean', default: false },
|
|
78
|
+
'no-wait': { type: 'boolean', default: false },
|
|
79
|
+
json: { type: 'boolean', default: false },
|
|
80
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
81
|
+
},
|
|
82
|
+
}).values
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`参数错误:${error instanceof Error ? error.message : String(error)}`,
|
|
86
|
+
{ cause: error },
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveMode(values) {
|
|
92
|
+
if (values.paste) {
|
|
93
|
+
const conflicting = ['pair', 'complete', 'no-wait', 'json'].find(
|
|
94
|
+
(flag) => values[flag] === true,
|
|
95
|
+
)
|
|
96
|
+
if (conflicting) {
|
|
97
|
+
throw new Error(`--paste 不能与 --${conflicting} 同时使用。`)
|
|
98
|
+
}
|
|
99
|
+
return 'paste'
|
|
100
|
+
}
|
|
101
|
+
if (values.complete) {
|
|
102
|
+
if (values['no-wait']) {
|
|
103
|
+
throw new Error('--complete 不能与 --no-wait 同时使用。')
|
|
104
|
+
}
|
|
105
|
+
return 'complete'
|
|
106
|
+
}
|
|
107
|
+
return 'pair'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function environmentCredentials(env = process.env) {
|
|
111
|
+
const appKey = env.REALSEE_GATEWAY_APP_KEY?.trim()
|
|
112
|
+
const appSecret = env.REALSEE_GATEWAY_APP_SECRET?.trim()
|
|
113
|
+
if (!appKey && !appSecret) return null
|
|
114
|
+
if (!appKey || !appSecret) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'REALSEE_GATEWAY_APP_KEY 与 REALSEE_GATEWAY_APP_SECRET 必须同时提供。',
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
const resourceCode = env.REALSEE_DEFAULT_RESOURCE_CODE?.trim()
|
|
120
|
+
const gatewayEndpoint = env.REALSEE_GATEWAY_ENDPOINT?.trim()
|
|
121
|
+
return normalizeGatewayCredentials({
|
|
122
|
+
appKey,
|
|
123
|
+
appSecret,
|
|
124
|
+
...(resourceCode ? { resourceCode } : {}),
|
|
125
|
+
...(gatewayEndpoint ? { gatewayEndpoint } : {}),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function configureFromEnvironment(config, { json }) {
|
|
130
|
+
await writeLocalConfig(config)
|
|
131
|
+
if (json !== true) {
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
'检测到 REALSEE_GATEWAY_APP_KEY / REALSEE_GATEWAY_APP_SECRET 环境变量(优先级最高),已跳过配对并写入 .env.development。\n',
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
process.stdout.write(
|
|
137
|
+
`${JSON.stringify({
|
|
138
|
+
ok: true,
|
|
139
|
+
data: {
|
|
140
|
+
status: 'configured',
|
|
141
|
+
source: 'environment',
|
|
142
|
+
app_key: maskAppKey(config.appKey),
|
|
143
|
+
path: '.env.development',
|
|
144
|
+
},
|
|
145
|
+
})}\n`,
|
|
146
|
+
)
|
|
147
|
+
return PAIRING_EXIT_CODES.OK
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
151
|
+
let values
|
|
152
|
+
try {
|
|
153
|
+
values = parseSetupArguments(argv)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return reportPairingFailure(error, { json: false })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (values.help) {
|
|
159
|
+
process.stdout.write(HELP)
|
|
160
|
+
return PAIRING_EXIT_CODES.OK
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let mode
|
|
164
|
+
try {
|
|
165
|
+
mode = resolveMode(values)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return reportPairingFailure(error, { json: values.json === true })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The paste fallback keeps its original interactive behaviour.
|
|
171
|
+
if (mode === 'paste') {
|
|
172
|
+
await runPasteFlow()
|
|
173
|
+
return PAIRING_EXIT_CODES.OK
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const json = values.json === true
|
|
177
|
+
try {
|
|
178
|
+
const config = environmentCredentials()
|
|
179
|
+
if (config) return await configureFromEnvironment(config, { json })
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return reportPairingFailure(error, { json })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (mode === 'complete') {
|
|
185
|
+
return await runPairingCompleteCommand({ json })
|
|
186
|
+
}
|
|
187
|
+
return await runPairingCommand({ noWait: values['no-wait'] === true, json })
|
|
188
|
+
}
|
|
189
|
+
|
|
42
190
|
const entrypoint = process.argv[1]
|
|
43
191
|
if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) {
|
|
44
|
-
main()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
192
|
+
main()
|
|
193
|
+
.then((exitCode) => {
|
|
194
|
+
if (typeof exitCode === 'number' && exitCode !== 0) {
|
|
195
|
+
process.exitCode = exitCode
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
.catch((error) => {
|
|
199
|
+
const message = error instanceof Error ? error.message : '未知错误。'
|
|
200
|
+
console.error(`本地配置失败:${message}`)
|
|
201
|
+
process.exitCode = 1
|
|
202
|
+
})
|
|
49
203
|
}
|
|
@@ -9,7 +9,7 @@ const PROJECT_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
|
9
9
|
|
|
10
10
|
export interface CreateAppOptions {
|
|
11
11
|
readonly workService: WorkService
|
|
12
|
-
readonly defaultResourceCode: string
|
|
12
|
+
readonly defaultResourceCode: string | undefined
|
|
13
13
|
readonly logger?: boolean
|
|
14
14
|
readonly serveFrontend?: boolean
|
|
15
15
|
readonly frontendRoot?: string
|
|
@@ -21,11 +21,12 @@ function requiredCredential(env: ServerEnv, key: string): string {
|
|
|
21
21
|
return value
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function
|
|
24
|
+
function optionalResourceCode(env: ServerEnv): string | undefined {
|
|
25
25
|
const value = env.REALSEE_DEFAULT_RESOURCE_CODE?.trim()
|
|
26
|
-
if (!nonEmptyString(value)
|
|
26
|
+
if (!nonEmptyString(value)) return undefined
|
|
27
|
+
if (!RESOURCE_CODE_PATTERN.test(value)) {
|
|
27
28
|
throw new Error(
|
|
28
|
-
'REALSEE_DEFAULT_RESOURCE_CODE
|
|
29
|
+
'REALSEE_DEFAULT_RESOURCE_CODE 格式无效,必须匹配 ^[A-Za-z0-9_-]{1,128}$。',
|
|
29
30
|
)
|
|
30
31
|
}
|
|
31
32
|
return value
|
|
@@ -59,6 +60,6 @@ export function loadServerConfig({
|
|
|
59
60
|
gatewayEndpoint: gatewayEndpoint.href.replace(/\/$/, ''),
|
|
60
61
|
appKey: requiredCredential(env, 'REALSEE_GATEWAY_APP_KEY'),
|
|
61
62
|
appSecret: requiredCredential(env, 'REALSEE_GATEWAY_APP_SECRET'),
|
|
62
|
-
defaultResourceCode:
|
|
63
|
+
defaultResourceCode: optionalResourceCode(env),
|
|
63
64
|
})
|
|
64
65
|
}
|
|
@@ -11,7 +11,7 @@ export interface ServerConfig {
|
|
|
11
11
|
readonly gatewayEndpoint: string
|
|
12
12
|
readonly appKey: string
|
|
13
13
|
readonly appSecret: string
|
|
14
|
-
readonly defaultResourceCode: string
|
|
14
|
+
readonly defaultResourceCode: string | undefined
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface RequestOptions {
|
|
@@ -5,7 +5,7 @@ const RESOURCE_CODE_PATTERN = '^[A-Za-z0-9_-]{1,128}$'
|
|
|
5
5
|
|
|
6
6
|
export interface WorkRoutesOptions {
|
|
7
7
|
readonly workService: WorkService
|
|
8
|
-
readonly defaultResourceCode: string
|
|
8
|
+
readonly defaultResourceCode: string | undefined
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
interface WorkRouteParams {
|
|
@@ -59,8 +59,19 @@ export function workRoutes(
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
app.get(
|
|
63
|
-
|
|
62
|
+
app.get(
|
|
63
|
+
'/api/work',
|
|
64
|
+
async (request, reply): Promise<WorkEnvelope | FastifyReply> => {
|
|
65
|
+
if (!defaultResourceCode) {
|
|
66
|
+
return reply.code(400).send({
|
|
67
|
+
error: 'RESOURCE_CODE_REQUIRED',
|
|
68
|
+
message:
|
|
69
|
+
'Provide resourceCode at /api/work/{resourceCode} or configure the optional REALSEE_DEFAULT_RESOURCE_CODE fallback.',
|
|
70
|
+
requestId: request.id,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
return getWork(request, reply, defaultResourceCode)
|
|
74
|
+
},
|
|
64
75
|
)
|
|
65
76
|
|
|
66
77
|
app.get(
|