@ranger1/dx 0.1.45 → 0.1.48
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/@opencode/agents/codex-reviewer.md +1 -1
- package/@opencode/agents/pr-context.md +1 -1
- package/@opencode/agents/pr-fix.md +1 -1
- package/@opencode/agents/pr-precheck.md +1 -1
- package/@opencode/commands/git-commit-and-pr.md +1 -1
- package/@opencode/commands/git-release.md +1 -1
- package/@opencode/commands/oh_attach.json +2 -2
- package/@opencode/commands/opencode_attach.json +3 -0
- package/lib/cli/args.js +21 -2
- package/lib/cli/commands/deploy.js +31 -2
- package/lib/cli/dx-cli.js +16 -6
- package/lib/cli/flags.js +6 -0
- package/lib/cli/help.js +13 -4
- package/lib/telegram-webhook.js +210 -23
- package/lib/vercel-deploy.js +19 -9
- package/package.json +1 -1
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
"variant": "high"
|
|
16
16
|
},
|
|
17
17
|
"librarian": {
|
|
18
|
-
"model": "
|
|
18
|
+
"model": "github-copilot/claude-sonnet-4.5"
|
|
19
19
|
},
|
|
20
20
|
"explore": {
|
|
21
|
-
"model": "
|
|
21
|
+
"model": "github-copilot/claude-sonnet-4.5"
|
|
22
22
|
},
|
|
23
23
|
"multimodal-looker": {
|
|
24
24
|
"model": "github-copilot/gemini-3-flash-preview"
|
package/lib/cli/args.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export function getCleanArgs(args = []) {
|
|
2
2
|
const result = []
|
|
3
|
-
let afterDoubleDash = false
|
|
4
3
|
for (const arg of args) {
|
|
5
4
|
if (arg === '--') {
|
|
6
|
-
afterDoubleDash = true
|
|
7
5
|
break
|
|
8
6
|
}
|
|
9
7
|
if (arg.startsWith('-')) continue
|
|
@@ -12,6 +10,27 @@ export function getCleanArgs(args = []) {
|
|
|
12
10
|
return result
|
|
13
11
|
}
|
|
14
12
|
|
|
13
|
+
// Like getCleanArgs(), but also strips values consumed by flags that expect a value.
|
|
14
|
+
// consumedFlagValueIndexes: Set<number> of indexes in the original argv that should be skipped.
|
|
15
|
+
export function getCleanArgsWithConsumedValues(args = [], consumedFlagValueIndexes = new Set()) {
|
|
16
|
+
const result = []
|
|
17
|
+
const consumed = consumedFlagValueIndexes instanceof Set
|
|
18
|
+
? consumedFlagValueIndexes
|
|
19
|
+
: new Set(consumedFlagValueIndexes || [])
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
const arg = args[i]
|
|
23
|
+
if (arg === '--') {
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
if (consumed.has(i)) continue
|
|
27
|
+
if (arg.startsWith('-')) continue
|
|
28
|
+
result.push(arg)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
export function getPassthroughArgs(args = []) {
|
|
16
35
|
const doubleDashIndex = args.indexOf('--')
|
|
17
36
|
if (doubleDashIndex === -1) return []
|
|
@@ -2,6 +2,28 @@ import { logger } from '../../logger.js'
|
|
|
2
2
|
import { envManager } from '../../env.js'
|
|
3
3
|
import { validateEnvironment } from '../../validate-env.js'
|
|
4
4
|
|
|
5
|
+
export function parseTelegramWebhookFlags(argv = []) {
|
|
6
|
+
const args = Array.isArray(argv) ? argv : []
|
|
7
|
+
|
|
8
|
+
const idx = args.indexOf('--webhook-path')
|
|
9
|
+
const webhookPath = idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined
|
|
10
|
+
|
|
11
|
+
const dryRun = args.includes('--webhook-dry-run') ? true : undefined
|
|
12
|
+
|
|
13
|
+
// 默认值由下游根据 environment 决定,这里只负责覆盖
|
|
14
|
+
const strict = args.includes('--strict-webhook')
|
|
15
|
+
? true
|
|
16
|
+
: args.includes('--no-strict-webhook')
|
|
17
|
+
? false
|
|
18
|
+
: undefined
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
webhookPath,
|
|
22
|
+
dryRun,
|
|
23
|
+
strict,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
5
27
|
export async function handleDeploy(cli, args) {
|
|
6
28
|
const target = args[0]
|
|
7
29
|
if (!target) {
|
|
@@ -59,7 +81,9 @@ export async function handleDeploy(cli, args) {
|
|
|
59
81
|
// 加载环境变量层,但不校验后端必需变量
|
|
60
82
|
const layeredEnv = envManager.collectEnvFromLayers(null, environment)
|
|
61
83
|
if (envManager.latestEnvWarnings && envManager.latestEnvWarnings.length > 0) {
|
|
62
|
-
envManager.latestEnvWarnings.forEach(message =>
|
|
84
|
+
envManager.latestEnvWarnings.forEach(message => {
|
|
85
|
+
logger.warn(message)
|
|
86
|
+
})
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
// 仅在目标变量不存在或是占位符时才使用 .env 文件的值
|
|
@@ -72,5 +96,10 @@ export async function handleDeploy(cli, args) {
|
|
|
72
96
|
envManager.syncEnvironments(environment)
|
|
73
97
|
|
|
74
98
|
const { deployToVercel } = await import('../../vercel-deploy.js')
|
|
75
|
-
|
|
99
|
+
|
|
100
|
+
const telegramWebhook = normalizedTarget === 'telegram-bot'
|
|
101
|
+
? parseTelegramWebhookFlags(cli.args)
|
|
102
|
+
: null
|
|
103
|
+
|
|
104
|
+
await deployToVercel(normalizedTarget, { environment, telegramWebhook })
|
|
76
105
|
}
|
package/lib/cli/dx-cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveTargetRequiredVars,
|
|
12
12
|
} from '../env-policy.js'
|
|
13
13
|
import { FLAG_DEFINITIONS, parseFlags } from './flags.js'
|
|
14
|
-
import { getCleanArgs } from './args.js'
|
|
14
|
+
import { getCleanArgs, getCleanArgsWithConsumedValues } from './args.js'
|
|
15
15
|
import { showHelp, showCommandHelp } from './help.js'
|
|
16
16
|
import { getPackageVersion } from '../version.js'
|
|
17
17
|
import {
|
|
@@ -175,7 +175,9 @@ class DxCli {
|
|
|
175
175
|
const envKey = this.normalizeEnvKey(environment)
|
|
176
176
|
const execFlags = { ...this.flags }
|
|
177
177
|
;['dev', 'development', 'prod', 'production', 'test', 'e2e', 'staging', 'stage'].forEach(
|
|
178
|
-
key =>
|
|
178
|
+
key => {
|
|
179
|
+
delete execFlags[key]
|
|
180
|
+
},
|
|
179
181
|
)
|
|
180
182
|
if (envKey === 'prod') execFlags.prod = true
|
|
181
183
|
else if (envKey === 'dev') execFlags.dev = true
|
|
@@ -329,8 +331,12 @@ class DxCli {
|
|
|
329
331
|
|
|
330
332
|
// 命令路由
|
|
331
333
|
async routeCommand() {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
+
const command = getCleanArgs(this.args)[0]
|
|
335
|
+
|
|
336
|
+
const allowedFlags = this.getAllowedFlags(command)
|
|
337
|
+
const consumedFlagValueIndexes = this.validateFlags(command, allowedFlags)
|
|
338
|
+
const cleanArgs = getCleanArgsWithConsumedValues(this.args, consumedFlagValueIndexes)
|
|
339
|
+
const [, ...subArgs] = cleanArgs
|
|
334
340
|
|
|
335
341
|
if (!command) {
|
|
336
342
|
showHelp()
|
|
@@ -747,13 +753,17 @@ class DxCli {
|
|
|
747
753
|
const portSet = new Set()
|
|
748
754
|
|
|
749
755
|
if (startConfig && Array.isArray(startConfig.ports)) {
|
|
750
|
-
startConfig.ports.forEach(port =>
|
|
756
|
+
startConfig.ports.forEach(port => {
|
|
757
|
+
this.addPortToSet(portSet, port)
|
|
758
|
+
})
|
|
751
759
|
}
|
|
752
760
|
|
|
753
761
|
if (envKey === 'dev') {
|
|
754
762
|
const legacyConfig = this.commands.dev?.[service]
|
|
755
763
|
if (legacyConfig && Array.isArray(legacyConfig.ports)) {
|
|
756
|
-
legacyConfig.ports.forEach(port =>
|
|
764
|
+
legacyConfig.ports.forEach(port => {
|
|
765
|
+
this.addPortToSet(portSet, port)
|
|
766
|
+
})
|
|
757
767
|
}
|
|
758
768
|
}
|
|
759
769
|
|
package/lib/cli/flags.js
CHANGED
|
@@ -37,6 +37,12 @@ export const FLAG_DEFINITIONS = {
|
|
|
37
37
|
lint: [
|
|
38
38
|
{ flag: '--fix' },
|
|
39
39
|
],
|
|
40
|
+
deploy: [
|
|
41
|
+
{ flag: '--webhook-path', expectsValue: true },
|
|
42
|
+
{ flag: '--webhook-dry-run' },
|
|
43
|
+
{ flag: '--strict-webhook' },
|
|
44
|
+
{ flag: '--no-strict-webhook' },
|
|
45
|
+
],
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export function parseFlags(args = []) {
|
package/lib/cli/help.js
CHANGED
|
@@ -184,17 +184,26 @@ script 子命令:
|
|
|
184
184
|
|
|
185
185
|
case 'deploy':
|
|
186
186
|
console.log(`
|
|
187
|
-
deploy 命令用法:
|
|
188
|
-
dx deploy <target> [环境标志]
|
|
187
|
+
deploy 命令用法:
|
|
188
|
+
dx deploy <target> [环境标志] [选项]
|
|
189
189
|
|
|
190
|
-
参数说明:
|
|
190
|
+
参数说明:
|
|
191
191
|
target: front, admin, telegram-bot, all
|
|
192
192
|
环境标志: --dev、--staging、--prod(默认 --staging)
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
Telegram Webhook(仅 target=telegram-bot 生效):
|
|
195
|
+
--webhook-path <path> 对外 webhook 路径(默认 /api/webhook)
|
|
196
|
+
--webhook-dry-run 只打印将设置的 URL,不调用 Telegram API
|
|
197
|
+
--strict-webhook 强制严格校验(Webhook 不生效则 deploy 失败)
|
|
198
|
+
--no-strict-webhook 关闭严格校验(仅告警)
|
|
199
|
+
|
|
200
|
+
常见示例:
|
|
195
201
|
dx deploy front --staging # 部署用户前端(staging)
|
|
196
202
|
dx deploy admin --prod # 部署管理后台(生产)
|
|
197
203
|
dx deploy telegram-bot --staging # 部署 Telegram Bot + 自动配置 Webhook
|
|
204
|
+
dx deploy telegram-bot --staging --webhook-path /webhook # 使用短路径(rewrite 到 /api/webhook)
|
|
205
|
+
dx deploy telegram-bot --prod --webhook-dry-run # 仅打印,不实际调用 Telegram
|
|
206
|
+
dx deploy telegram-bot --prod --no-strict-webhook # 生产环境也仅告警(不推荐)
|
|
198
207
|
dx deploy all --staging # 串行部署 front + admin
|
|
199
208
|
`)
|
|
200
209
|
return
|
package/lib/telegram-webhook.js
CHANGED
|
@@ -2,12 +2,89 @@ import { execSync } from 'node:child_process'
|
|
|
2
2
|
import { logger } from './logger.js'
|
|
3
3
|
import { envManager } from './env.js'
|
|
4
4
|
|
|
5
|
+
function normalizeWebhookPath(raw) {
|
|
6
|
+
const s = String(raw || '').trim()
|
|
7
|
+
if (!s) return '/api/webhook'
|
|
8
|
+
if (s.startsWith('/')) return s
|
|
9
|
+
return `/${s}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeDeployUrl(raw) {
|
|
13
|
+
const s = String(raw || '')
|
|
14
|
+
const m = s.match(/(https?:\/\/)?([a-z0-9-]+\.vercel\.app)\b/i)
|
|
15
|
+
if (!m) return null
|
|
16
|
+
return `https://${m[2]}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseDeployUrlFromDeployOutput(output) {
|
|
20
|
+
const s = String(output || '')
|
|
21
|
+
const matches = [...s.matchAll(/(https?:\/\/)?([a-z0-9-]+\.vercel\.app)\b/gi)]
|
|
22
|
+
if (matches.length === 0) return null
|
|
23
|
+
const last = matches[matches.length - 1]
|
|
24
|
+
return `https://${last[2]}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseDeployUrlFromVercelListOutput(output, projectNameHint) {
|
|
28
|
+
const lines = String(output || '')
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.map(l => l.trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
|
|
33
|
+
const isReady = line => /\b(Ready|READY)\b/.test(line)
|
|
34
|
+
const hasUrl = line => /\b[a-z0-9-]+\.vercel\.app\b/i.test(line)
|
|
35
|
+
const pickUrl = line => {
|
|
36
|
+
const m = line.match(/(https?:\/\/)?([a-z0-9-]+\.vercel\.app)\b/i)
|
|
37
|
+
return m ? `https://${m[2]}` : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const hint = projectNameHint ? String(projectNameHint) : ''
|
|
41
|
+
if (hint) {
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
if (!line.includes(hint)) continue
|
|
44
|
+
if (!isReady(line)) continue
|
|
45
|
+
if (!hasUrl(line)) continue
|
|
46
|
+
const url = pickUrl(line)
|
|
47
|
+
if (url) return url
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (!isReady(line)) continue
|
|
53
|
+
if (!hasUrl(line)) continue
|
|
54
|
+
const url = pickUrl(line)
|
|
55
|
+
if (url) return url
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
5
61
|
/**
|
|
6
62
|
* 处理 Telegram Bot 部署后的 Webhook 配置
|
|
7
63
|
*/
|
|
8
|
-
export async function handleTelegramBotDeploy(environment, projectId, orgId, token) {
|
|
64
|
+
export async function handleTelegramBotDeploy(environment, projectId, orgId, token, options = {}) {
|
|
9
65
|
logger.step('配置 Telegram Webhook...')
|
|
10
66
|
|
|
67
|
+
const {
|
|
68
|
+
deployOutput,
|
|
69
|
+
projectNameHint,
|
|
70
|
+
webhookPath: webhookPathOverride,
|
|
71
|
+
dryRun: dryRunOverride,
|
|
72
|
+
strict: strictOverride,
|
|
73
|
+
} = options || {}
|
|
74
|
+
|
|
75
|
+
const strictDefault = environment !== 'development'
|
|
76
|
+
|
|
77
|
+
const strictEnv = process.env.DX_TELEGRAM_WEBHOOK_STRICT != null
|
|
78
|
+
? !['0', 'false', 'no'].includes(String(process.env.DX_TELEGRAM_WEBHOOK_STRICT).toLowerCase())
|
|
79
|
+
: undefined
|
|
80
|
+
|
|
81
|
+
const strict = strictOverride ?? strictEnv ?? strictDefault
|
|
82
|
+
|
|
83
|
+
const dryRunEnv = ['1', 'true', 'yes'].includes(String(process.env.DX_TELEGRAM_WEBHOOK_DRY_RUN || '').toLowerCase())
|
|
84
|
+
const dryRun = dryRunOverride ?? (dryRunEnv ? true : false)
|
|
85
|
+
|
|
86
|
+
const webhookPath = normalizeWebhookPath(webhookPathOverride ?? process.env.DX_TELEGRAM_WEBHOOK_PATH ?? '/api/webhook')
|
|
87
|
+
|
|
11
88
|
// 1. 验证必需环境变量
|
|
12
89
|
const botToken = process.env.TELEGRAM_BOT_TOKEN
|
|
13
90
|
const webhookSecret = process.env.TELEGRAM_BOT_WEBHOOK_SECRET
|
|
@@ -25,21 +102,42 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
|
|
|
25
102
|
missingVars.forEach(v => {
|
|
26
103
|
logger.error(` - ${v}`)
|
|
27
104
|
})
|
|
105
|
+
|
|
106
|
+
if (strict) {
|
|
107
|
+
throw new Error('Telegram Webhook 配置失败:缺少必需环境变量')
|
|
108
|
+
}
|
|
109
|
+
|
|
28
110
|
logger.warn('跳过 Webhook 配置,请手动设置')
|
|
29
111
|
return
|
|
30
112
|
}
|
|
31
113
|
|
|
32
114
|
try {
|
|
33
115
|
// 2. 获取 Vercel 部署 URL
|
|
34
|
-
const deploymentUrl = await getLatestDeploymentUrl(
|
|
116
|
+
const deploymentUrl = await getLatestDeploymentUrl({
|
|
117
|
+
projectId,
|
|
118
|
+
orgId,
|
|
119
|
+
token,
|
|
120
|
+
environment,
|
|
121
|
+
deployOutput,
|
|
122
|
+
projectNameHint,
|
|
123
|
+
})
|
|
35
124
|
if (!deploymentUrl) {
|
|
125
|
+
if (strict) {
|
|
126
|
+
throw new Error('无法获取 Vercel 部署 URL')
|
|
127
|
+
}
|
|
128
|
+
|
|
36
129
|
logger.error('无法获取 Vercel 部署 URL,跳过 Webhook 配置')
|
|
37
130
|
return
|
|
38
131
|
}
|
|
39
132
|
|
|
40
|
-
const webhookUrl = `${deploymentUrl}
|
|
133
|
+
const webhookUrl = `${deploymentUrl}${webhookPath}`
|
|
41
134
|
logger.info(`Webhook URL: ${webhookUrl}`)
|
|
42
135
|
|
|
136
|
+
if (dryRun) {
|
|
137
|
+
logger.warn('DX_TELEGRAM_WEBHOOK_DRY_RUN=1,已跳过 setWebhook/getWebhookInfo 调用')
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
43
141
|
// 3. 调用 Telegram API 设置 Webhook
|
|
44
142
|
const telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook`
|
|
45
143
|
const payload = JSON.stringify({
|
|
@@ -65,10 +163,15 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
|
|
|
65
163
|
logger.info(`Webhook URL: ${webhookUrl}`)
|
|
66
164
|
|
|
67
165
|
// 4. 验证 Webhook 状态
|
|
68
|
-
await verifyWebhook(botToken)
|
|
166
|
+
await verifyWebhook(botToken, webhookUrl, { strict })
|
|
69
167
|
}
|
|
70
168
|
else {
|
|
71
|
-
|
|
169
|
+
const desc = result.description || '未知错误'
|
|
170
|
+
if (strict) {
|
|
171
|
+
throw new Error(`Telegram Webhook 设置失败: ${desc}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
logger.error(`Telegram Webhook 设置失败: ${desc}`)
|
|
72
175
|
logger.info('请手动执行以下命令(不要把明文 token/secret 写进日志):')
|
|
73
176
|
const manualPayload = JSON.stringify({
|
|
74
177
|
url: webhookUrl,
|
|
@@ -81,7 +184,11 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
|
|
|
81
184
|
}
|
|
82
185
|
}
|
|
83
186
|
catch (error) {
|
|
84
|
-
|
|
187
|
+
const message = error?.message || String(error)
|
|
188
|
+
logger.error(`Webhook 配置失败: ${message}`)
|
|
189
|
+
|
|
190
|
+
if (strict) throw error
|
|
191
|
+
|
|
85
192
|
logger.warn('请手动设置 Webhook(参考 apps/telegram-bot/README.md)')
|
|
86
193
|
}
|
|
87
194
|
}
|
|
@@ -89,9 +196,86 @@ export async function handleTelegramBotDeploy(environment, projectId, orgId, tok
|
|
|
89
196
|
/**
|
|
90
197
|
* 获取最新部署的 URL
|
|
91
198
|
*/
|
|
92
|
-
async function getLatestDeploymentUrl(
|
|
199
|
+
async function getLatestDeploymentUrl({
|
|
200
|
+
projectId,
|
|
201
|
+
orgId,
|
|
202
|
+
token,
|
|
203
|
+
environment,
|
|
204
|
+
deployOutput,
|
|
205
|
+
projectNameHint,
|
|
206
|
+
}) {
|
|
207
|
+
const fromDeploy = parseDeployUrlFromDeployOutput(deployOutput)
|
|
208
|
+
if (fromDeploy) return fromDeploy
|
|
209
|
+
|
|
210
|
+
const fromApi = await getDeploymentUrlFromVercelApi({ projectId, orgId, token, environment })
|
|
211
|
+
if (fromApi) return fromApi
|
|
212
|
+
|
|
213
|
+
const fromList = await getDeploymentUrlFromVercelList({ orgId, token, projectNameHint })
|
|
214
|
+
if (fromList) return fromList
|
|
215
|
+
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function pickDeploymentUrlFromVercelApiResponse(json) {
|
|
220
|
+
const deployments = json?.deployments
|
|
221
|
+
if (!Array.isArray(deployments)) return null
|
|
222
|
+
|
|
223
|
+
for (const d of deployments) {
|
|
224
|
+
const url = d?.url
|
|
225
|
+
if (!url) continue
|
|
226
|
+
const state = d?.state || d?.readyState
|
|
227
|
+
if (state && String(state).toUpperCase() !== 'READY') continue
|
|
228
|
+
const normalized = normalizeDeployUrl(url)
|
|
229
|
+
if (normalized) return normalized
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function getDeploymentUrlFromVercelApi({ projectId, orgId, token, environment }) {
|
|
236
|
+
try {
|
|
237
|
+
const qs = new URLSearchParams({
|
|
238
|
+
projectId: String(projectId),
|
|
239
|
+
state: 'READY',
|
|
240
|
+
limit: '10',
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// dx 的 deploy 实现里:staging/production 都会传 --prod,因此对应 Vercel 的 production target。
|
|
244
|
+
// development 环境若需要兜底查询,则不强制 target(避免与 Vercel CLI/REST 字段差异耦合)。
|
|
245
|
+
if (environment !== 'development') {
|
|
246
|
+
qs.set('target', 'production')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (orgId) {
|
|
250
|
+
const scope = String(orgId)
|
|
251
|
+
if (scope.startsWith('team_')) qs.set('teamId', scope)
|
|
252
|
+
else qs.set('slug', scope)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const url = `https://api.vercel.com/v6/deployments?${qs.toString()}`
|
|
256
|
+
const res = await fetch(url, {
|
|
257
|
+
method: 'GET',
|
|
258
|
+
headers: {
|
|
259
|
+
Authorization: `Bearer ${token}`,
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
logger.warn(`Vercel API 获取部署列表失败: HTTP ${res.status}`)
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const json = await res.json()
|
|
269
|
+
return pickDeploymentUrlFromVercelApiResponse(json)
|
|
270
|
+
} catch (error) {
|
|
271
|
+
logger.warn(`Vercel API 获取部署列表失败: ${error?.message || String(error)}`)
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function getDeploymentUrlFromVercelList({ orgId, token, projectNameHint }) {
|
|
93
277
|
try {
|
|
94
|
-
const cmd = ['vercel', '
|
|
278
|
+
const cmd = ['vercel', 'list', orgId ? `--scope=${orgId}` : '']
|
|
95
279
|
.filter(Boolean)
|
|
96
280
|
.join(' ')
|
|
97
281
|
|
|
@@ -99,24 +283,13 @@ async function getLatestDeploymentUrl(projectId, orgId, token, environment) {
|
|
|
99
283
|
encoding: 'utf8',
|
|
100
284
|
env: {
|
|
101
285
|
...process.env,
|
|
102
|
-
// 不通过 CLI args 传递 token,避免出现在错误信息/日志中
|
|
103
286
|
VERCEL_TOKEN: token,
|
|
104
287
|
},
|
|
105
288
|
})
|
|
106
|
-
const deployments = JSON.parse(output)
|
|
107
|
-
|
|
108
|
-
// 根据环境筛选部署
|
|
109
|
-
const targetEnv = environment === 'production' ? 'production' : 'preview'
|
|
110
|
-
const latest = deployments.find(d =>
|
|
111
|
-
d.projectId === projectId
|
|
112
|
-
&& d.target === targetEnv
|
|
113
|
-
&& d.state === 'READY',
|
|
114
|
-
)
|
|
115
289
|
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
logger.warn(`获取部署 URL 失败: ${error.message}`)
|
|
290
|
+
return parseDeployUrlFromVercelListOutput(output, projectNameHint)
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.warn(`vercel list 获取部署 URL 失败: ${error?.message || String(error)}`)
|
|
120
293
|
return null
|
|
121
294
|
}
|
|
122
295
|
}
|
|
@@ -124,7 +297,8 @@ async function getLatestDeploymentUrl(projectId, orgId, token, environment) {
|
|
|
124
297
|
/**
|
|
125
298
|
* 验证 Webhook 配置
|
|
126
299
|
*/
|
|
127
|
-
async function verifyWebhook(botToken) {
|
|
300
|
+
async function verifyWebhook(botToken, expectedWebhookUrl, options = {}) {
|
|
301
|
+
const { strict = false } = options || {}
|
|
128
302
|
try {
|
|
129
303
|
const cmd = `curl -s "https://api.telegram.org/bot${botToken}/getWebhookInfo"`
|
|
130
304
|
const response = execSync(cmd, { encoding: 'utf8' })
|
|
@@ -138,9 +312,22 @@ async function verifyWebhook(botToken) {
|
|
|
138
312
|
if (info.last_error_message) {
|
|
139
313
|
logger.warn(` 最后错误: ${info.last_error_message}`)
|
|
140
314
|
}
|
|
315
|
+
|
|
316
|
+
if (expectedWebhookUrl && info.url !== expectedWebhookUrl) {
|
|
317
|
+
const message = `Webhook 未生效:期望 ${expectedWebhookUrl},实际 ${info.url || '(empty)'}`
|
|
318
|
+
if (strict) throw new Error(message)
|
|
319
|
+
logger.warn(message)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
const desc = result?.description || '未知错误'
|
|
324
|
+
const message = `getWebhookInfo 失败: ${desc}`
|
|
325
|
+
if (strict) throw new Error(message)
|
|
326
|
+
logger.warn(message)
|
|
141
327
|
}
|
|
142
328
|
}
|
|
143
329
|
catch (error) {
|
|
330
|
+
if (strict) throw error
|
|
144
331
|
logger.warn('无法验证 Webhook 状态')
|
|
145
332
|
}
|
|
146
333
|
}
|
package/lib/vercel-deploy.js
CHANGED
|
@@ -87,21 +87,21 @@ export async function deployPrebuiltWithFallback(options) {
|
|
|
87
87
|
} = options || {}
|
|
88
88
|
|
|
89
89
|
try {
|
|
90
|
-
await run(baseArgs, { env, cwd })
|
|
91
|
-
return { usedArchive: false }
|
|
90
|
+
const result = await run(baseArgs, { env, cwd })
|
|
91
|
+
return { usedArchive: false, result }
|
|
92
92
|
} catch (e) {
|
|
93
93
|
if (!isMissingFilesError(e)) throw e
|
|
94
94
|
onMissingFiles(e)
|
|
95
95
|
cleanupArchiveParts()
|
|
96
96
|
const archiveArgs = baseArgs.slice()
|
|
97
97
|
archiveArgs.splice(2, 0, '--archive=tgz')
|
|
98
|
-
await run(archiveArgs, { env, cwd })
|
|
99
|
-
return { usedArchive: true }
|
|
98
|
+
const result = await run(archiveArgs, { env, cwd })
|
|
99
|
+
return { usedArchive: true, result }
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export async function deployToVercel(target, options = {}) {
|
|
104
|
-
const { environment = 'staging' } = options
|
|
104
|
+
const { environment = 'staging', telegramWebhook = null } = options
|
|
105
105
|
|
|
106
106
|
// 校验环境参数
|
|
107
107
|
if (!ALLOWED_ENVIRONMENTS.includes(environment)) {
|
|
@@ -264,17 +264,27 @@ export async function deployToVercel(target, options = {}) {
|
|
|
264
264
|
baseDeployArgs.push('--scope', orgId)
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
await deployPrebuiltWithFallback({
|
|
267
|
+
const deployResult = await deployPrebuiltWithFallback({
|
|
268
268
|
baseArgs: baseDeployArgs,
|
|
269
269
|
env: envVars,
|
|
270
270
|
cwd: process.cwd(),
|
|
271
271
|
})
|
|
272
|
-
logger.success(`${t} 部署成功`)
|
|
273
272
|
|
|
274
|
-
|
|
273
|
+
const deployOutput = [deployResult?.result?.stdout, deployResult?.result?.stderr]
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.join('\n')
|
|
276
|
+
|
|
277
|
+
// Telegram Bot 部署成功后自动设置 Webhook(并做严格校验)
|
|
275
278
|
if (t === 'telegram-bot') {
|
|
276
279
|
const { handleTelegramBotDeploy } = await import('./telegram-webhook.js')
|
|
277
|
-
await handleTelegramBotDeploy(environment, projectId, orgId, token
|
|
280
|
+
await handleTelegramBotDeploy(environment, projectId, orgId, token, {
|
|
281
|
+
deployOutput,
|
|
282
|
+
projectNameHint: 'telegram-bot',
|
|
283
|
+
...(telegramWebhook || {}),
|
|
284
|
+
})
|
|
285
|
+
logger.success(`${t} 部署成功(Webhook 已校验)`)
|
|
286
|
+
} else {
|
|
287
|
+
logger.success(`${t} 部署成功`)
|
|
278
288
|
}
|
|
279
289
|
} catch (error) {
|
|
280
290
|
const message = error?.message || String(error)
|