@cloudbase/cloudbase-mcp 2.26.0 → 2.28.0

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/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import * as __WEBPACK_EXTERNAL_MODULE_https__ from "https";
13
13
  import * as __WEBPACK_EXTERNAL_MODULE_net__ from "net";
14
14
  import * as __WEBPACK_EXTERNAL_MODULE_fs__ from "fs";
15
15
  import * as __WEBPACK_EXTERNAL_MODULE__modelcontextprotocol_sdk_server_mcp_js_45c326f0__ from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import * as __WEBPACK_EXTERNAL_MODULE_node_path_02319fef__ from "node:path";
16
17
  import * as __WEBPACK_EXTERNAL_MODULE_child_process__ from "child_process";
17
18
  import * as __WEBPACK_EXTERNAL_MODULE_ws__ from "ws";
18
19
  import * as __WEBPACK_EXTERNAL_MODULE__cloudbase_manager_node_6a52ea40__ from "@cloudbase/manager-node";
@@ -23,6 +24,7 @@ import * as __WEBPACK_EXTERNAL_MODULE_crypto__ from "crypto";
23
24
  import * as __WEBPACK_EXTERNAL_MODULE_fs_promises_f8dae9d1__ from "fs/promises";
24
25
  import * as __WEBPACK_EXTERNAL_MODULE_os__ from "os";
25
26
  import * as __WEBPACK_EXTERNAL_MODULE_winston__ from "winston";
27
+ import * as __WEBPACK_EXTERNAL_MODULE_node_fs_75ed2103__ from "node:fs";
26
28
  /******/ var __webpack_modules__ = ({
27
29
 
28
30
  /***/ 622:
@@ -30,8 +32,17 @@ import * as __WEBPACK_EXTERNAL_MODULE_winston__ from "winston";
30
32
 
31
33
 
32
34
  Object.defineProperty(exports, "__esModule", ({ value: true }));
33
- exports.DEFAULT_CREATE_ENV_RESOURCES = exports.CREATE_ENV_RESOURCE_VALUES = void 0;
35
+ exports.GATEWAY_ENV_QPS_DEFAULT_RESOURCE_ID = exports.ENV_METRIC_PERIOD_VALUES = exports.ENV_METRIC_NAME_VALUES = exports.ENV_USAGE_MODULE_VALUES = exports.DEFAULT_CREATE_ENV_RESOURCES = exports.CREATE_ENV_RESOURCE_VALUES = void 0;
34
36
  exports.resolveCreateEnvResources = resolveCreateEnvResources;
37
+ exports.resolveEnvUsageModules = resolveEnvUsageModules;
38
+ exports.extractAccountCircleDate = extractAccountCircleDate;
39
+ exports.resolveEnvUsageDateRange = resolveEnvUsageDateRange;
40
+ exports.formatEnvMetricTime = formatEnvMetricTime;
41
+ exports.resolveEnvMetricName = resolveEnvMetricName;
42
+ exports.resolveEnvMetricPeriod = resolveEnvMetricPeriod;
43
+ exports.resolveEnvMetricTimeRange = resolveEnvMetricTimeRange;
44
+ exports.resolveEnvMetricResourceId = resolveEnvMetricResourceId;
45
+ exports.summarizeEnvMetricCurve = summarizeEnvMetricCurve;
35
46
  exports.simplifyEnvList = simplifyEnvList;
36
47
  exports.isUsableNoSqlDatabaseEntry = isUsableNoSqlDatabaseEntry;
37
48
  exports.registerEnvTools = registerEnvTools;
@@ -63,6 +74,245 @@ function resolveCreateEnvResources(resources) {
63
74
  }
64
75
  return [...exports.DEFAULT_CREATE_ENV_RESOURCES];
65
76
  }
77
+ /**
78
+ * Resource-usage modules aligned with tcb CLI `USAGE_MODULES`
79
+ * (`tcb env usage` / `tcb env info --type`).
80
+ */
81
+ exports.ENV_USAGE_MODULE_VALUES = [
82
+ "FLEXDB",
83
+ "TDSQL",
84
+ "SCF",
85
+ "EKS",
86
+ "COS",
87
+ "AI",
88
+ "HOSTING",
89
+ "Auth",
90
+ "APIInvocation",
91
+ "HTTPInvocation",
92
+ "VM",
93
+ "Workflow",
94
+ "Other",
95
+ ];
96
+ const ENV_USAGE_MODULE_SET = new Set(exports.ENV_USAGE_MODULE_VALUES);
97
+ const ENV_USAGE_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
98
+ function resolveEnvUsageModules(type) {
99
+ if (!Array.isArray(type) || type.length === 0) {
100
+ return [...exports.ENV_USAGE_MODULE_VALUES];
101
+ }
102
+ const normalized = type
103
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
104
+ .filter(Boolean);
105
+ const invalid = normalized.filter((item) => !ENV_USAGE_MODULE_SET.has(item));
106
+ if (invalid.length > 0) {
107
+ throw new Error(`无效的用量模块 type: ${invalid.join(", ")}。可选值:${exports.ENV_USAGE_MODULE_VALUES.join(", ")}`);
108
+ }
109
+ return normalized;
110
+ }
111
+ function extractAccountCircleDate(rawTime) {
112
+ if (typeof rawTime !== "string" || rawTime.trim().length === 0) {
113
+ return undefined;
114
+ }
115
+ const datePart = rawTime.trim().split(/\s+/)[0];
116
+ return ENV_USAGE_DATE_RE.test(datePart) ? datePart : undefined;
117
+ }
118
+ function normalizeUsageDateInput(value) {
119
+ return typeof value === "string" && value.trim().length > 0
120
+ ? value.trim()
121
+ : undefined;
122
+ }
123
+ function resolveEnvUsageDateRange(options) {
124
+ const startFromParams = normalizeUsageDateInput(options.startDate);
125
+ const endFromParams = normalizeUsageDateInput(options.endDate);
126
+ if (startFromParams || endFromParams) {
127
+ if (!startFromParams || !endFromParams) {
128
+ throw new Error("查询资源用量时 startDate 与 endDate 必须同时提供,格式为 YYYY-MM-DD。");
129
+ }
130
+ if (!ENV_USAGE_DATE_RE.test(startFromParams) || !ENV_USAGE_DATE_RE.test(endFromParams)) {
131
+ throw new Error("startDate / endDate 格式必须为 YYYY-MM-DD。");
132
+ }
133
+ if (startFromParams > endFromParams) {
134
+ throw new Error("startDate 不能晚于 endDate。");
135
+ }
136
+ return {
137
+ startDate: startFromParams,
138
+ endDate: endFromParams,
139
+ dateSource: "params",
140
+ };
141
+ }
142
+ const startDate = extractAccountCircleDate(options.accountCircle?.StartTime);
143
+ const endDate = extractAccountCircleDate(options.accountCircle?.EndTime);
144
+ if (!startDate || !endDate) {
145
+ throw new Error("无法从计费周期推导用量日期范围。请显式传入 startDate/endDate(YYYY-MM-DD),或确认环境计费周期可用。");
146
+ }
147
+ return { startDate, endDate, dateSource: "accountCircle" };
148
+ }
149
+ /**
150
+ * Monitor metric names for queryEnv(action=metrics).
151
+ * Subset of TCB DescribeCurveData MetricName from the CloudBase monitor catalog
152
+ * (gateway/env QPS, functions, NoSQL, MySQL, CloudRun).
153
+ */
154
+ exports.ENV_METRIC_NAME_VALUES = [
155
+ "GatewayTraceEnvQPS",
156
+ "EnvQPSAll",
157
+ "FunctionInvocation",
158
+ "FunctionError",
159
+ "FunctionTimeout",
160
+ "FunctionThrottle",
161
+ "FunctionDuration",
162
+ "FunctionConcurrentExecutions",
163
+ "DbRead",
164
+ "DbWrite",
165
+ "DbSizepkg",
166
+ "MysqlCpuUsageRate",
167
+ "MysqlMemoryUse",
168
+ "MysqlStorageUsage",
169
+ "MysqlQps",
170
+ "MysqlSlowQueries",
171
+ "MysqlDbConnections",
172
+ "TkeCpuUsedService",
173
+ "TkeMemUsedService",
174
+ "TkeQPSService",
175
+ "TkeHttpErrorService",
176
+ "TkeInvokeNumService",
177
+ ];
178
+ exports.ENV_METRIC_PERIOD_VALUES = [300, 3600, 86400];
179
+ /** Console default ResourceID for environment-level gateway QPS. */
180
+ exports.GATEWAY_ENV_QPS_DEFAULT_RESOURCE_ID = "all|:|all|:|all|:|all";
181
+ const ENV_METRIC_NAME_SET = new Set(exports.ENV_METRIC_NAME_VALUES);
182
+ const ENV_METRIC_PERIOD_SET = new Set(exports.ENV_METRIC_PERIOD_VALUES);
183
+ const ENV_METRICS_REQUIRING_RESOURCE_ID = new Set([
184
+ "TkeCpuUsedService",
185
+ "TkeMemUsedService",
186
+ "TkeQPSService",
187
+ "TkeHttpErrorService",
188
+ "TkeInvokeNumService",
189
+ ]);
190
+ const ENV_METRIC_TIME_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
191
+ const ENV_METRIC_MIN_RANGE_MS = 5 * 60 * 1000;
192
+ function padMetricTimeUnit(value) {
193
+ return value.toString().padStart(2, "0");
194
+ }
195
+ function formatEnvMetricTime(date) {
196
+ return `${date.getFullYear()}-${padMetricTimeUnit(date.getMonth() + 1)}-${padMetricTimeUnit(date.getDate())} ${padMetricTimeUnit(date.getHours())}:${padMetricTimeUnit(date.getMinutes())}:${padMetricTimeUnit(date.getSeconds())}`;
197
+ }
198
+ function resolveEnvMetricName(metricName) {
199
+ const normalized = typeof metricName === "string" && metricName.trim().length > 0
200
+ ? metricName.trim()
201
+ : undefined;
202
+ if (!normalized) {
203
+ throw new Error(`查询监控指标时 metricName 为必填参数。可选值:${exports.ENV_METRIC_NAME_VALUES.join(", ")}`);
204
+ }
205
+ if (!ENV_METRIC_NAME_SET.has(normalized)) {
206
+ throw new Error(`无效的 metricName: ${normalized}。可选值:${exports.ENV_METRIC_NAME_VALUES.join(", ")}`);
207
+ }
208
+ return normalized;
209
+ }
210
+ function resolveEnvMetricPeriod(period) {
211
+ if (period === undefined || period === null || period === "") {
212
+ return undefined;
213
+ }
214
+ const numericPeriod = typeof period === "number"
215
+ ? period
216
+ : typeof period === "string" && /^\d+$/.test(period.trim())
217
+ ? Number(period.trim())
218
+ : Number.NaN;
219
+ if (!ENV_METRIC_PERIOD_SET.has(numericPeriod)) {
220
+ throw new Error(`period 仅支持 300、3600、86400(秒)。当前值:${String(period)}`);
221
+ }
222
+ return numericPeriod;
223
+ }
224
+ function resolveEnvMetricTimeRange(options) {
225
+ const startFromParams = typeof options.startTime === "string" && options.startTime.trim().length > 0
226
+ ? options.startTime.trim()
227
+ : undefined;
228
+ const endFromParams = typeof options.endTime === "string" && options.endTime.trim().length > 0
229
+ ? options.endTime.trim()
230
+ : undefined;
231
+ if (startFromParams || endFromParams) {
232
+ if (!startFromParams || !endFromParams) {
233
+ throw new Error("查询监控指标时 startTime 与 endTime 必须同时提供,格式为 YYYY-MM-DD HH:mm:ss。");
234
+ }
235
+ if (!ENV_METRIC_TIME_RE.test(startFromParams) || !ENV_METRIC_TIME_RE.test(endFromParams)) {
236
+ throw new Error("startTime / endTime 格式必须为 YYYY-MM-DD HH:mm:ss。");
237
+ }
238
+ const startMs = Date.parse(startFromParams.replace(" ", "T"));
239
+ const endMs = Date.parse(endFromParams.replace(" ", "T"));
240
+ if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
241
+ throw new Error("startTime / endTime 无法解析为有效时间。");
242
+ }
243
+ if (endMs - startMs < ENV_METRIC_MIN_RANGE_MS) {
244
+ throw new Error("结束时间需要晚于开始时间至少五分钟(监控最小粒度为 5 分钟)。");
245
+ }
246
+ return {
247
+ startTime: startFromParams,
248
+ endTime: endFromParams,
249
+ timeSource: "params",
250
+ };
251
+ }
252
+ const now = options.now ?? new Date();
253
+ const start = new Date(now.getTime() - 24 * 60 * 60 * 1000);
254
+ return {
255
+ startTime: formatEnvMetricTime(start),
256
+ endTime: formatEnvMetricTime(now),
257
+ timeSource: "defaultLast24h",
258
+ };
259
+ }
260
+ function resolveEnvMetricResourceId(metricName, resourceID) {
261
+ const normalized = typeof resourceID === "string" && resourceID.trim().length > 0
262
+ ? resourceID.trim()
263
+ : undefined;
264
+ if (normalized) {
265
+ return normalized;
266
+ }
267
+ if (metricName === "GatewayTraceEnvQPS") {
268
+ return exports.GATEWAY_ENV_QPS_DEFAULT_RESOURCE_ID;
269
+ }
270
+ if (ENV_METRICS_REQUIRING_RESOURCE_ID.has(metricName)) {
271
+ throw new Error(`查询 ${metricName} 时 resourceID 为必填(云托管服务名)。请先 queryCloudRun(action="list") 获取服务名后再查询。`);
272
+ }
273
+ return undefined;
274
+ }
275
+ function summarizeEnvMetricCurve(curve) {
276
+ const newValues = Array.isArray(curve.NewValues) ? curve.NewValues : [];
277
+ const values = Array.isArray(curve.Values) ? curve.Values : [];
278
+ const times = Array.isArray(curve.Time) ? curve.Time : [];
279
+ const series = (newValues.length > 0 ? newValues : values).filter((item) => typeof item === "number" && Number.isFinite(item));
280
+ if (series.length === 0) {
281
+ return {
282
+ sampleCount: 0,
283
+ max: null,
284
+ min: null,
285
+ avg: null,
286
+ latest: null,
287
+ peakTimestamp: null,
288
+ allZero: true,
289
+ };
290
+ }
291
+ let max = series[0];
292
+ let min = series[0];
293
+ let sum = 0;
294
+ let peakIndex = 0;
295
+ for (let i = 0; i < series.length; i++) {
296
+ const value = series[i];
297
+ sum += value;
298
+ if (value > max) {
299
+ max = value;
300
+ peakIndex = i;
301
+ }
302
+ if (value < min) {
303
+ min = value;
304
+ }
305
+ }
306
+ return {
307
+ sampleCount: series.length,
308
+ max,
309
+ min,
310
+ avg: sum / series.length,
311
+ latest: series[series.length - 1],
312
+ peakTimestamp: typeof times[peakIndex] === "number" ? times[peakIndex] : null,
313
+ allZero: series.every((value) => value === 0),
314
+ };
315
+ }
66
316
  /**
67
317
  * Simplify environment list data by keeping only essential fields for AI assistant
68
318
  * This reduces token consumption when returning environment lists via MCP tools
@@ -916,6 +1166,19 @@ function buildEnvQueryErrorMessage(error, action) {
916
1166
  if (hasNetworkError) {
917
1167
  suggestions.push("网络错误:请检查网络连接,稍后重试。");
918
1168
  }
1169
+ if (action === "usage" && suggestions.length === 0) {
1170
+ suggestions.push("查询环境资源用量失败,建议:");
1171
+ suggestions.push("1. 先调用 auth(action=\"status\") 确认登录状态;未登录则 auth(action=\"start_auth\")");
1172
+ suggestions.push("2. 使用 queryEnv(action=\"list\") 确认 envId 正确且可访问");
1173
+ suggestions.push(`3. 再调用 queryEnv(action=\"usage\", envId=\"<EnvId>\");可用 type 过滤模块(${exports.ENV_USAGE_MODULE_VALUES.join(", ")})`);
1174
+ }
1175
+ if (action === "metrics" && suggestions.length === 0) {
1176
+ suggestions.push("查询环境监控指标失败,建议:");
1177
+ suggestions.push("1. 先调用 auth(action=\"status\") 确认登录状态;未登录则 auth(action=\"start_auth\")");
1178
+ suggestions.push("2. 使用 queryEnv(action=\"list\") 确认 envId 正确且可访问");
1179
+ suggestions.push(`3. 再调用 queryEnv(action=\"metrics\", envId=\"<EnvId>\", metricName=\"GatewayTraceEnvQPS\");metricName 可选值:${exports.ENV_METRIC_NAME_VALUES.join(", ")}`);
1180
+ suggestions.push("4. startTime/endTime 格式为 YYYY-MM-DD HH:mm:ss,须成对传入;period 仅 300/3600/86400");
1181
+ }
919
1182
  // If no specific pattern matched, provide general guidance
920
1183
  if (suggestions.length === 0) {
921
1184
  suggestions.push("查询环境信息时出错,建议:");
@@ -1826,8 +2089,8 @@ function registerEnvTools(server) {
1826
2089
  }
1827
2090
  });
1828
2091
  } // end: wxide guard for auth tool
1829
- // queryEnv - 环境查询(合并 listEnvs + getEnvInfo + getEnvAuthDomains)
1830
- const queryEnvHandler = async ({ action, alias, aliasExact, envId, limit, offset, fields, }) => {
2092
+ // queryEnv - 环境查询(合并 listEnvs + getEnvInfo + getEnvAuthDomains + usage + metrics)
2093
+ const queryEnvHandler = async ({ action, alias, aliasExact, envId, limit, offset, fields, type, startDate, endDate, needUsageDetails, metricName, startTime, endTime, period, resourceID, subresourceID, }) => {
1831
2094
  try {
1832
2095
  let result;
1833
2096
  switch (action) {
@@ -1989,6 +2252,94 @@ function registerEnvTools(server) {
1989
2252
  };
1990
2253
  }
1991
2254
  break;
2255
+ case "usage": {
2256
+ // Explicit EnvId is required (DescribeEnvPostpayPackage / credits APIs reject missing EnvId).
2257
+ const usageEnvId = normalizeOptionalToolString(envId);
2258
+ if (!usageEnvId) {
2259
+ throw new Error('查询资源用量时 envId 为必填参数。请先调用 queryEnv(action="list") 获取 EnvId,再调用 queryEnv(action="usage", envId="<EnvId>")。');
2260
+ }
2261
+ const modules = resolveEnvUsageModules(type);
2262
+ const includeDetails = typeof needUsageDetails === "boolean" ? needUsageDetails : true;
2263
+ const cloudbaseUsage = await getManagerForEnvQuery(usageEnvId);
2264
+ const accountCircle = await cloudbaseUsage.env.describeEnvAccountCircle({
2265
+ EnvId: usageEnvId,
2266
+ });
2267
+ (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, accountCircle);
2268
+ const dateRange = resolveEnvUsageDateRange({
2269
+ startDate,
2270
+ endDate,
2271
+ accountCircle,
2272
+ });
2273
+ const usageDetail = await cloudbaseUsage.env.describeCreditsUsageDetail({
2274
+ EnvId: usageEnvId,
2275
+ Modules: modules,
2276
+ StartDate: dateRange.startDate,
2277
+ EndDate: dateRange.endDate,
2278
+ NeedUsageDetails: includeDetails,
2279
+ });
2280
+ (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, usageDetail);
2281
+ result = {
2282
+ EnvId: usageEnvId,
2283
+ Modules: modules,
2284
+ StartDate: dateRange.startDate,
2285
+ EndDate: dateRange.endDate,
2286
+ DateSource: dateRange.dateSource,
2287
+ NeedUsageDetails: includeDetails,
2288
+ AccountCircle: {
2289
+ StartTime: accountCircle?.StartTime,
2290
+ EndTime: accountCircle?.EndTime,
2291
+ HistoryTime: accountCircle?.HistoryTime ?? [],
2292
+ RequestId: accountCircle?.RequestId,
2293
+ },
2294
+ Usages: usageDetail?.Usages ?? [],
2295
+ RequestId: usageDetail?.RequestId,
2296
+ };
2297
+ break;
2298
+ }
2299
+ case "metrics": {
2300
+ const metricsEnvId = normalizeOptionalToolString(envId);
2301
+ if (!metricsEnvId) {
2302
+ throw new Error('查询监控指标时 envId 为必填参数。请先调用 queryEnv(action="list") 获取 EnvId,再调用 queryEnv(action="metrics", envId="<EnvId>", metricName="GatewayTraceEnvQPS")。');
2303
+ }
2304
+ const resolvedMetricName = resolveEnvMetricName(metricName);
2305
+ const timeRange = resolveEnvMetricTimeRange({ startTime, endTime });
2306
+ const resolvedPeriod = resolveEnvMetricPeriod(period);
2307
+ const resolvedResourceId = resolveEnvMetricResourceId(resolvedMetricName, resourceID);
2308
+ const resolvedSubresourceId = normalizeOptionalToolString(subresourceID);
2309
+ const cloudbaseMetrics = await getManagerForEnvQuery(metricsEnvId);
2310
+ if (typeof cloudbaseMetrics?.monitor?.describeCurveData !== "function") {
2311
+ throw new Error("当前 CloudBase Manager 不支持 monitor.describeCurveData。请升级 @cloudbase/manager-node。");
2312
+ }
2313
+ const curveParams = {
2314
+ MetricName: resolvedMetricName,
2315
+ StartTime: timeRange.startTime,
2316
+ EndTime: timeRange.endTime,
2317
+ };
2318
+ if (resolvedPeriod !== undefined) {
2319
+ curveParams.Period = resolvedPeriod;
2320
+ }
2321
+ if (resolvedResourceId) {
2322
+ curveParams.ResourceID = resolvedResourceId;
2323
+ }
2324
+ if (resolvedSubresourceId) {
2325
+ curveParams.SubresourceID = resolvedSubresourceId;
2326
+ }
2327
+ const curve = await cloudbaseMetrics.monitor.describeCurveData(curveParams);
2328
+ (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, curve);
2329
+ result = {
2330
+ EnvId: metricsEnvId,
2331
+ MetricName: resolvedMetricName,
2332
+ StartTime: timeRange.startTime,
2333
+ EndTime: timeRange.endTime,
2334
+ TimeSource: timeRange.timeSource,
2335
+ Period: resolvedPeriod ?? curve?.Period,
2336
+ ResourceID: resolvedResourceId,
2337
+ SubresourceID: resolvedSubresourceId,
2338
+ Curve: curve,
2339
+ Summary: summarizeEnvMetricCurve(curve ?? {}),
2340
+ };
2341
+ break;
2342
+ }
1992
2343
  default:
1993
2344
  throw new Error(`不支持的查询类型: ${action}`);
1994
2345
  }
@@ -2021,20 +2372,63 @@ function registerEnvTools(server) {
2021
2372
  // Register primary tool name (queryEnv)
2022
2373
  const queryEnvToolSchema = {
2023
2374
  title: "CloudBase 环境查询",
2024
- description: "查询 CloudBase 环境相关信息,支持查询环境列表、指定环境详情和安全域名。(曾用名:envQuery、listEnvs、getEnvInfo、getEnvAuthDomains)当 action=list 时,会按 DescribeEnvs 语义做列表/筛选,标准返回字段为 EnvId、Alias、Status、EnvType、Region、PackageId、PackageName、IsDefault,并支持通过 fields 白名单裁剪这些字段;aliasExact=true 时会按别名精确筛选,避免把前缀相近的环境误当作候选;即使传入 envId,action=list 也只返回摘要,不会返回完整资源明细或 expiry。如需查询某个已知 EnvId 对应环境的详细信息(包括资源字段和计费信息),必须使用 action=info 并传入目标环境的 envId 参数。action=info 会在可用时补充 BillingInfo(如 ExpireTime、PayMode、IsAutoRenew 等计费字段)。\n\n🔍 action=info 还会派生三个用于后端选型的字段:\n- `EnvInfo.RuntimeMode`:'postgresql' 或 'nosql',表示新业务建议默认使用的后端(PG 已开通时为 postgresql,否则为 nosql)。\n- `EnvInfo.RuntimeBackends`:`{postgresql, nosql, mysql}` 三个布尔值,描述当前环境实际并存的后端。\n- `EnvInfo.RuntimeModeHints`:每个后端对应的 API/工具/skill 提示。\n\n🌐 action=info 还会在不改写 `StaticStorages[].StaticDomain`(云 API 名义域名)的前提下,投影网关路由 Enable 状态:`StaticStorages[].staticDomainRouteEnabled` 与 `EnvInfo.staticDomainRouteEnabled`(与 queryHosting websiteConfig 同源)。`false` 表示默认静态域名根路由已禁用(访问会返回 GATEWAY_ROUTE_DISABLED),勿把名义域名当成可达 URL。\n\nAI 在写业务/权限/存储代码前必须先看这三项:PG 模式下新业务推荐 `app.rdb()` + RLS(`managePgDatabase action=execute` 跑 `CREATE POLICY`)+ pgstore;已存在的 NoSQL 集合 / 旧 storage / `managePermissions(resourceType=\"noSqlDatabase\")` 在 PG 环境下仍然有效。真正不适用的是 MySQL:当 `RuntimeBackends.mysql === false` 时,`manageMysqlDatabase` / `queryMysqlDatabase` / `relational-database-mcp-cloudbase` skill 都不该使用。",
2375
+ description: "查询 CloudBase 环境相关信息,支持查询环境列表、指定环境详情、安全域名、资源用量与监控指标。(曾用名:envQuery、listEnvs、getEnvInfo、getEnvAuthDomains)当 action=list 时,会按 DescribeEnvs 语义做列表/筛选,标准返回字段为 EnvId、Alias、Status、EnvType、Region、PackageId、PackageName、IsDefault,并支持通过 fields 白名单裁剪这些字段;aliasExact=true 时会按别名精确筛选,避免把前缀相近的环境误当作候选;即使传入 envId,action=list 也只返回摘要,不会返回完整资源明细或 expiry。如需查询某个已知 EnvId 对应环境的详细信息(包括资源字段和计费信息),必须使用 action=info 并传入目标环境的 envId 参数。action=info 会在可用时补充 BillingInfo(如 ExpireTime、PayMode、IsAutoRenew 等计费字段)。\n\n📊 action=usage 对齐 tcb env usage/info:透传 Manager SDK describeEnvAccountCircle + describeCreditsUsageDetail,返回计费周期与各模块资源点用量(FLEXDB/SCF/COS 等)。envId 必填;type 可选过滤模块;未传 startDate/endDate 时自动使用当前计费周期。\n\n📈 action=metrics 对齐 TCB DescribeCurveData(manager.monitor.describeCurveData,不是云监控 GetMonitorData):查询环境/网关 QPS、云函数调用与错误、数据库 CPU/内存/磁盘、云托管 CPU/QPS 等时序。envId 与 metricName 必填;startTime/endTime 格式 YYYY-MM-DD HH:mm:ss,须成对传入,不传则默认最近 24 小时;period 仅 300/3600/86400。GatewayTraceEnvQPS 未传 resourceID 时自动填环境级 all|:|all|:|all|:|all;云托管 Tke* 指标必须传服务名 resourceID。禁止用 callCloudApi 猜测监控 Action。\n\n🔍 action=info 还会派生三个用于后端选型的字段:\n- `EnvInfo.RuntimeMode`:'postgresql' 或 'nosql',表示新业务建议默认使用的后端(PG 已开通时为 postgresql,否则为 nosql)。\n- `EnvInfo.RuntimeBackends`:`{postgresql, nosql, mysql}` 三个布尔值,描述当前环境实际并存的后端。\n- `EnvInfo.RuntimeModeHints`:每个后端对应的 API/工具/skill 提示。\n\n🌐 action=info 还会在不改写 `StaticStorages[].StaticDomain`(云 API 名义域名)的前提下,投影网关路由 Enable 状态:`StaticStorages[].staticDomainRouteEnabled` 与 `EnvInfo.staticDomainRouteEnabled`(与 queryHosting websiteConfig 同源)。`false` 表示默认静态域名根路由已禁用(访问会返回 GATEWAY_ROUTE_DISABLED),勿把名义域名当成可达 URL。\n\nAI 在写业务/权限/存储代码前必须先看这三项:PG 模式下新业务推荐 `app.rdb()` + RLS(`managePgDatabase action=execute` 跑 `CREATE POLICY`)+ pgstore;已存在的 NoSQL 集合 / 旧 storage / `managePermissions(resourceType=\"noSqlDatabase\")` 在 PG 环境下仍然有效。真正不适用的是 MySQL:当 `RuntimeBackends.mysql === false` 时,`manageMysqlDatabase` / `queryMysqlDatabase` / `relational-database-mcp-cloudbase` skill 都不该使用。",
2025
2376
  inputSchema: {
2026
2377
  action: zod_1.z
2027
- .enum(["list", "info", "domains"])
2028
- .describe("查询类型:list=环境列表/摘要筛选(按 DescribeEnvs 语义筛选,支持通过 envId 筛选,返回 EnvId、Alias、Status、EnvType、Region、PackageId、PackageName、IsDefault,不支持 expiry),info=指定环境的详细信息(必须传入 envId,返回资源字段和计费信息),domains=安全域名列表"),
2378
+ .enum(["list", "info", "domains", "usage", "metrics"])
2379
+ .describe("查询类型:list=环境列表/摘要筛选(按 DescribeEnvs 语义筛选,支持通过 envId 筛选,返回 EnvId、Alias、Status、EnvType、Region、PackageId、PackageName、IsDefault,不支持 expiry),info=指定环境的详细信息(必须传入 envId,返回资源字段和计费信息),domains=安全域名列表,usage=环境资源用量(必须传入 envId,对齐 tcb env usage/info),metrics=环境监控时序(必须传入 envId 与 metricName,对齐 TCB DescribeCurveData)"),
2029
2380
  alias: zod_1.z.string().optional().describe("按环境别名筛选。action=list 时可选"),
2030
2381
  aliasExact: zod_1.z.boolean().optional().describe("按环境别名精确筛选。action=list 时可选;与 alias 配合使用"),
2031
- envId: zod_1.z.string().optional().describe("按环境 ID 筛选。action=list 时可选(仅按 DescribeEnvs 语义做筛选,仍返回摘要);action=info 时必填(返回该环境的详细信息,包含资源字段和计费信息)。如果任务已经给出了明确的 EnvId 并要求查询详情,请直接使用 action=info + envId,而不是 action=list"),
2382
+ envId: zod_1.z
2383
+ .string()
2384
+ .optional()
2385
+ .describe("环境 ID。action=list 时可选(仅按 DescribeEnvs 语义做筛选,仍返回摘要);action=info / action=usage / action=metrics 时必填。"),
2032
2386
  limit: zod_1.z.number().int().positive().optional().describe("返回数量上限。action=list 时可选"),
2033
2387
  offset: zod_1.z.number().int().min(0).optional().describe("分页偏移。action=list 时可选"),
2034
2388
  fields: zod_1.z
2035
2389
  .array(zod_1.z.enum(DEFAULT_ENV_FIELDS))
2036
2390
  .optional()
2037
2391
  .describe("返回字段白名单。仅支持 EnvId、Alias、Status、EnvType、Region、PackageId、PackageName、IsDefault。action=list 时可选"),
2392
+ type: zod_1.z
2393
+ .array(zod_1.z.enum(exports.ENV_USAGE_MODULE_VALUES))
2394
+ .optional()
2395
+ .describe("用量模块过滤。仅 action=usage 时有效;不传则查询全部模块。可选值对齐 tcb CLI:FLEXDB、TDSQL、SCF、EKS、COS、AI、HOSTING、Auth、APIInvocation、HTTPInvocation、VM、Workflow、Other。"),
2396
+ startDate: zod_1.z
2397
+ .string()
2398
+ .optional()
2399
+ .describe("用量开始日期(YYYY-MM-DD)。仅 action=usage 时有效;与 endDate 成对传入。不传则使用当前计费周期。"),
2400
+ endDate: zod_1.z
2401
+ .string()
2402
+ .optional()
2403
+ .describe("用量结束日期(YYYY-MM-DD)。仅 action=usage 时有效;与 startDate 成对传入。不传则使用当前计费周期。"),
2404
+ needUsageDetails: zod_1.z
2405
+ .boolean()
2406
+ .optional()
2407
+ .describe("是否返回每日用量明细。仅 action=usage 时有效;默认 true。"),
2408
+ metricName: zod_1.z
2409
+ .enum(exports.ENV_METRIC_NAME_VALUES)
2410
+ .optional()
2411
+ .describe("监控指标名。仅 action=metrics 时有效且必填。GatewayTraceEnvQPS/EnvQPSAll=环境与网关 QPS;FunctionInvocation/FunctionError/FunctionTimeout/FunctionThrottle=云函数调用、错误、超时、限流;DbRead/DbWrite/DbSizepkg=文档库读写与容量;MysqlCpuUsageRate/MysqlMemoryUse/MysqlStorageUsage=SQL 库 CPU/内存/磁盘;TkeCpuUsedService/TkeQPSService/TkeHttpErrorService=云托管 CPU/QPS/错误。"),
2412
+ startTime: zod_1.z
2413
+ .string()
2414
+ .optional()
2415
+ .describe("监控开始时间(YYYY-MM-DD HH:mm:ss)。仅 action=metrics 时有效;与 endTime 成对传入。不传则默认最近 24 小时。结束时间须晚于开始时间至少五分钟。"),
2416
+ endTime: zod_1.z
2417
+ .string()
2418
+ .optional()
2419
+ .describe("监控结束时间(YYYY-MM-DD HH:mm:ss)。仅 action=metrics 时有效;与 startTime 成对传入。不传则默认最近 24 小时。"),
2420
+ period: zod_1.z
2421
+ .union([zod_1.z.literal(300), zod_1.z.literal(3600), zod_1.z.literal(86400)])
2422
+ .optional()
2423
+ .describe("统计周期(秒)。仅 action=metrics 时有效;仅支持 300、3600、86400。不传则由后端按时间范围自动选择。时间范围 ≤1 天不可用 86400;>3 天不可用 300。"),
2424
+ resourceID: zod_1.z
2425
+ .string()
2426
+ .optional()
2427
+ .describe("资源 ID。仅 action=metrics 时有效。云函数传函数名,文档库传集合名,云托管必须传服务名;GatewayTraceEnvQPS 不传则使用环境级 all|:|all|:|all|:|all。"),
2428
+ subresourceID: zod_1.z
2429
+ .string()
2430
+ .optional()
2431
+ .describe("子资源 ID。仅 action=metrics 时有效;查询云托管某版本监控时传入版本名。"),
2038
2432
  },
2039
2433
  annotations: {
2040
2434
  readOnlyHint: true,
@@ -2550,7 +2944,11 @@ function registerEnvTools(server) {
2550
2944
 
2551
2945
 
2552
2946
  Object.defineProperty(exports, "__esModule", ({ value: true }));
2947
+ exports.CAM_AUTH_ERROR_PATTERN = void 0;
2948
+ exports.assertTcbCloudRunActionAllowed = assertTcbCloudRunActionAllowed;
2553
2949
  exports.removeEmptyStringParams = removeEmptyStringParams;
2950
+ exports.buildCamAuthGuidance = buildCamAuthGuidance;
2951
+ exports.isCamAuthError = isCamAuthError;
2554
2952
  exports.buildCapiErrorMessage = buildCapiErrorMessage;
2555
2953
  exports.registerCapiTools = registerCapiTools;
2556
2954
  const zod_1 = __webpack_require__(2971);
@@ -2561,6 +2959,7 @@ const CLOUDBASE_CONTROL_PLANE_DOC_URL = "https://cloud.tencent.com/document/prod
2561
2959
  const CLOUDBASE_DEPENDENCY_API_DOC_URL = "https://cloud.tencent.com/document/product/876/34808";
2562
2960
  const ALLOWED_SERVICES = [
2563
2961
  "tcb",
2962
+ "tcbr",
2564
2963
  "scf",
2565
2964
  "sts",
2566
2965
  "cam",
@@ -2568,6 +2967,34 @@ const ALLOWED_SERVICES = [
2568
2967
  "cdn",
2569
2968
  "vpc",
2570
2969
  ];
2970
+ /**
2971
+ * Legacy tcb small-tenant CloudRun (CloudBase Run) API family — blocked.
2972
+ *
2973
+ * CloudRun must use tcbr (CreateCloudRunEnv / CreateCloudRunServer). tcb
2974
+ * CreateCloudBaseRunResource is the 2018 small-tenant open API and is no longer
2975
+ * publicly listed; calling it on an env without a large-tenant record creates
2976
+ * wrong small-tenant services/versions (user incident 2026-08-13; ATO research
2977
+ * efec7cc5). Describe/Delete of the same family are blocked too. Matching is
2978
+ * case-insensitive so Action casing variants cannot bypass the check.
2979
+ */
2980
+ const TCB_CLOUDRUN_FORBIDDEN_ACTIONS = [
2981
+ "CreateCloudBaseRunResource",
2982
+ "DescribeCloudBaseRunResource",
2983
+ "DeleteCloudBaseRunResource",
2984
+ ];
2985
+ const TCB_CLOUDRUN_FORBIDDEN_ACTIONS_LOWER = new Set(TCB_CLOUDRUN_FORBIDDEN_ACTIONS.map((name) => name.toLowerCase()));
2986
+ const TCB_CLOUDRUN_FORBIDDEN_HINT = `云托管(CloudBase Run)统一走 tcbr 新逻辑(CreateCloudRunEnv / CreateCloudRunServer),` +
2987
+ `不要使用 tcb 旧小租户接口(${TCB_CLOUDRUN_FORBIDDEN_ACTIONS.join(" / ")})。` +
2988
+ `新环境请先初始化云托管:callCloudApi(service="tcbr", version="2022-02-17", action="CreateCloudRunEnv", params={EnvId:"..."}),` +
2989
+ `再通过 manageCloudRun(action="deploy") 创建服务;查询单个环境基础信息/是否已开通云托管用 callCloudApi(service="tcbr", version="2022-02-17", action="DescribeEnvBaseInfo", params={EnvId:"..."}),查询环境列表/资源信息用 DescribeCloudRunEnvs。`;
2990
+ /**
2991
+ * Reject legacy tcb small-tenant CloudRun APIs; throw with tcbr guidance when blocked.
2992
+ */
2993
+ function assertTcbCloudRunActionAllowed(service, action) {
2994
+ if (service === "tcb" && TCB_CLOUDRUN_FORBIDDEN_ACTIONS_LOWER.has(action.toLowerCase())) {
2995
+ throw new Error(`[${service}/${action}] 已禁用:${TCB_CLOUDRUN_FORBIDDEN_HINT}`);
2996
+ }
2997
+ }
2571
2998
  function levenshteinDistance(left, right) {
2572
2999
  const rows = left.length + 1;
2573
3000
  const cols = right.length + 1;
@@ -2643,11 +3070,26 @@ function formatTcbParamsTypeHint(action) {
2643
3070
  return `参数类型参考:\n\`\`\`ts\n${entry.paramsType}\n\`\`\``;
2644
3071
  }
2645
3072
  function buildCapiDocGuidance(service) {
2646
- if (service === "tcb" || service === "lowcode" || service === "scf") {
3073
+ if (service === "tcb" || service === "tcbr" || service === "lowcode" || service === "scf") {
2647
3074
  return `优先查阅 CloudBase API 概览 ${CLOUDBASE_CONTROL_PLANE_DOC_URL} 与云开发依赖资源接口指引 ${CLOUDBASE_DEPENDENCY_API_DOC_URL}。`;
2648
3075
  }
2649
3076
  return `请优先核对对应官方云 API 文档;若你的场景其实是通过 HTTP 协议直接集成 auth/functions/cloudrun/storage/mysqldb 等 CloudBase 业务 API,请优先使用 OpenAPI / Swagger 或 searchKnowledgeBase(mode="openapi"),不要继续猜测管控面 Action。`;
2650
3077
  }
3078
+ /** Match CAM / authorization failures from Tencent Cloud control-plane APIs. */
3079
+ exports.CAM_AUTH_ERROR_PATTERN = /UnauthorizedOperation|AuthFailure|not authorized|cam.*denied|Forbidden/i;
3080
+ /**
3081
+ * Guidance when control-plane calls fail due to CAM / API Key scope limits.
3082
+ * Shared by callCloudApi and manageCloudRun error builders.
3083
+ */
3084
+ function buildCamAuthGuidance() {
3085
+ return ("这通常是 CAM 权限不足(常见于 API Key 登录仅授权数据面:DB/函数/存储)。请任选其一:" +
3086
+ "1) 改用 device code 登录管控面:auth(action=\"start_auth\", authMode=\"device\");" +
3087
+ "2) 或使用腾讯云 SecretId/SecretKey(确认子账号已授 CAM 策略,如 QcloudTCBFullAccess、QcloudVPCReadOnlyAccess);" +
3088
+ "3) 确认目标资源属于当前登录账号。");
3089
+ }
3090
+ function isCamAuthError(message) {
3091
+ return exports.CAM_AUTH_ERROR_PATTERN.test(message);
3092
+ }
2651
3093
  function buildCapiErrorMessage(service, action, error) {
2652
3094
  const baseMessage = error instanceof Error ? error.message : String(error);
2653
3095
  const suggestions = [];
@@ -2655,6 +3097,10 @@ function buildCapiErrorMessage(service, action, error) {
2655
3097
  const hasInvalidActionError = /invalid or not found|does not exist|not recognized/i.test(baseMessage);
2656
3098
  const hasParameterError = /parameter\s+`?.+?`?\s+is not recognized|MissingParameter|missing parameter|missing required/i.test(baseMessage);
2657
3099
  const hasInvalidParameterValueError = /invalid parameter value/i.test(baseMessage);
3100
+ const hasCamAuthError = isCamAuthError(baseMessage);
3101
+ if (hasCamAuthError) {
3102
+ suggestions.push(buildCamAuthGuidance());
3103
+ }
2658
3104
  if (hasInvalidActionError) {
2659
3105
  suggestions.push(`Action \`${action}\` 可能不存在或不对外开放。请不要继续猜测 Action 名称,先确认 service=\`${service}\` 下该 Action 在当前 API 版本是否真实存在。`);
2660
3106
  if (service === "tcb") {
@@ -2733,15 +3179,21 @@ function registerCapiTools(server) {
2733
3179
  **云函数**: \`DescribeFunctions\`、\`CreateFunction\`、\`UpdateFunctionCode\`、\`DeleteFunction\`
2734
3180
  **数据库**: \`CreateMySQLInstance\`、\`DescribeMySQLInstances\`、\`DestroyMySQLInstance\`
2735
3181
 
3182
+ ⚠️ 云托管(CloudBase Run)统一走 tcbr service(CreateCloudRunEnv / CreateCloudRunServer / DescribeEnvBaseInfo / DescribeCloudRunEnvs,version="2022-02-17"),tcb 旧小租户接口 CreateCloudBaseRunResource 等已被禁用;部署请用 manageCloudRun。查询单个环境基础信息/是否已开通云托管用 DescribeEnvBaseInfo(EnvId 必填),查询环境列表及资源信息用 DescribeCloudRunEnvs(EnvId 可选过滤)。
3183
+
2736
3184
  销毁环境时,常见做法是至少带上 \`EnvId\` 和 \`BypassCheck: true\`,如果环境已经处于隔离期再按文档补 \`IsForce: true\`。`,
2737
3185
  inputSchema: {
2738
3186
  service: zod_1.z
2739
3187
  .enum(ALLOWED_SERVICES)
2740
- .describe("选择要访问的服务。可选:tcb、scf、sts、cam、lowcode、cdn、vpc。对于 tcb / scf / lowcode 等 CloudBase 管控面 Action,请优先查官方文档,不要直接猜测 Action。"),
3188
+ .describe("选择要访问的服务。可选:tcb、tcbr、scf、sts、cam、lowcode、cdn、vpc。对于 tcb / scf / lowcode 等 CloudBase 管控面 Action,请优先查官方文档,不要直接猜测 Action。云托管统一走 tcbr(version 需传 2022-02-17)。"),
2741
3189
  action: zod_1.z
2742
3190
  .string()
2743
3191
  .min(1)
2744
- .describe("具体 Action 名称,需符合对应服务的官方 API 定义。若不确定正确 Action,请先查官方文档;不要用近义词或历史命名进行猜测。tcb 常用 Action:环境管理 CreateEnv/ModifyEnv/DescribeEnvs/DestroyEnv、用户管理 CreateUser/ModifyUser/DescribeUserList/DeleteUsers、认证配置 EditAuthConfig、云函数 DescribeFunctions/CreateFunction、数据库 CreateMySQLInstance 等。"),
3192
+ .describe("具体 Action 名称,需符合对应服务的官方 API 定义。若不确定正确 Action,请先查官方文档;不要用近义词或历史命名进行猜测。tcb 常用 Action:环境管理 CreateEnv/ModifyEnv/DescribeEnvs/DestroyEnv、用户管理 CreateUser/ModifyUser/DescribeUserList/DeleteUsers、认证配置 EditAuthConfig、云函数 DescribeFunctions/CreateFunction、数据库 CreateMySQLInstance 等。tcbr 常用 Action:CreateCloudRunEnv(初始化云托管)、DescribeEnvBaseInfo(查询单个环境基础信息,EnvId 必填)、DescribeCloudRunEnvs(查询环境列表/资源信息,EnvId 可选过滤)、CreateCloudRunServer/DescribeCloudRunServers。"),
3193
+ version: zod_1.z
3194
+ .string()
3195
+ .optional()
3196
+ .describe("API 版本(可选)。缺省时按 service 使用 SDK 内置默认版本;tcbr 必须传 \"2022-02-17\"(否则请求缺少 X-TC-Version 会失败)。示例:service=\"tcbr\", version=\"2022-02-17\", action=\"CreateCloudRunEnv\", params={EnvId:\"env-xxx\",PackageType:\"Standard\"}。"),
2745
3197
  params: zod_1.z
2746
3198
  .record(zod_1.z.any())
2747
3199
  .optional()
@@ -2754,10 +3206,11 @@ function registerCapiTools(server) {
2754
3206
  openWorldHint: true,
2755
3207
  category: CATEGORY,
2756
3208
  },
2757
- }, async ({ service, action, params, }) => {
3209
+ }, async ({ service, action, params, version, }) => {
2758
3210
  if (!ALLOWED_SERVICES.includes(service)) {
2759
3211
  throw new Error(`Service ${service} is not allowed. Allowed services: ${ALLOWED_SERVICES.join(", ")}`);
2760
3212
  }
3213
+ assertTcbCloudRunActionAllowed(service, action);
2761
3214
  const cloudbase = await getManager();
2762
3215
  if (['1', 'true'].includes(process.env.CLOUDBASE_EVALUATE_MODE ?? '')) {
2763
3216
  if (service === 'lowcode') {
@@ -2765,9 +3218,13 @@ function registerCapiTools(server) {
2765
3218
  }
2766
3219
  if (service === 'tcb') {
2767
3220
  const tcbCapiForbidList = [
2768
- // 未明确对外的云API
3221
+ // Cloud APIs not clearly public
2769
3222
  'DescribeStorageACL', 'ModifyStorageACL', 'DescribeSecurityRule',
2770
- // 要下线的云API
3223
+ // Legacy small-tenant CloudRun open/query/delete (no longer public; also blocked by assertTcbCloudRunActionAllowed)
3224
+ "CreateCloudBaseRunResource",
3225
+ "DescribeCloudBaseRunResource",
3226
+ "DeleteCloudBaseRunResource",
3227
+ // Cloud APIs scheduled for retirement
2771
3228
  "ListTables",
2772
3229
  "DescribeCloudBaseGWAPI",
2773
3230
  "DescribeCloudBaseGWService",
@@ -2786,7 +3243,7 @@ function registerCapiTools(server) {
2786
3243
  let result;
2787
3244
  try {
2788
3245
  const cleanedParams = params ? removeEmptyStringParams(params) : {};
2789
- result = await cloudbase.commonService(service).call({
3246
+ result = await cloudbase.commonService(service, version).call({
2790
3247
  Action: action,
2791
3248
  Param: cleanedParams,
2792
3249
  });
@@ -2903,7 +3360,7 @@ ${envIdSection}
2903
3360
  ## 环境信息
2904
3361
  - 操作系统: ${os_1.default.type()} ${os_1.default.release()}
2905
3362
  - Node.js版本: ${process.version}
2906
- - MCP 版本:${process.env.npm_package_version || ( true ? "2.26.0" : 0)}
3363
+ - MCP 版本:${process.env.npm_package_version || ( true ? "2.28.0" : 0)}
2907
3364
  - 系统架构: ${os_1.default.arch()}
2908
3365
  - 时间: ${new Date().toISOString()}
2909
3366
  - 请求ID: ${requestId}
@@ -2984,7 +3441,7 @@ function createWrappedHandler(name, handler, server) {
2984
3441
  requestId: (typeof error === 'object' && error && 'requestId' in error) ? error.requestId : '',
2985
3442
  ide: server.ide || process.env.INTEGRATION_IDE || ''
2986
3443
  });
2987
- const mcpVersion = true ? "2.26.0" : 0;
3444
+ const mcpVersion = true ? "2.28.0" : 0;
2988
3445
  const requestIdSuffix = requestId ? `\n🆔 RequestId: ${requestId}` : '';
2989
3446
  const enhancedErrorMessage = `${errorMessage}${requestIdSuffix}\n\n📦 CloudBase MCP v${mcpVersion}\n🔗 遇到问题?请复制以下链接到浏览器打开\n即可自动携带错误详情快速创建 GitHub Issue:\n${issueLink}`;
2990
3447
  // 创建新的错误对象,保持原有的错误类型但更新消息
@@ -3139,7 +3596,6 @@ const mcp_js_1 = __webpack_require__(4619);
3139
3596
  const databaseNoSQL_js_1 = __webpack_require__(3903);
3140
3597
  const databasePG_js_1 = __webpack_require__(6473);
3141
3598
  const databaseSQL_js_1 = __webpack_require__(9782);
3142
- const download_js_1 = __webpack_require__(9097);
3143
3599
  const env_js_1 = __webpack_require__(622);
3144
3600
  const functions_js_1 = __webpack_require__(5936);
3145
3601
  const hosting_js_1 = __webpack_require__(7279);
@@ -3160,7 +3616,7 @@ const logs_js_1 = __webpack_require__(2322);
3160
3616
  const permissions_js_1 = __webpack_require__(6485);
3161
3617
  const cloud_mode_js_1 = __webpack_require__(9684);
3162
3618
  const logger_js_1 = __webpack_require__(3039);
3163
- const tencent_cloud_js_1 = __webpack_require__(1454);
3619
+ const site_map_js_1 = __webpack_require__(8107);
3164
3620
  const tool_result_js_1 = __webpack_require__(9835);
3165
3621
  const tool_wrapper_js_1 = __webpack_require__(1363);
3166
3622
  // 默认插件列表
@@ -3182,13 +3638,15 @@ const DEFAULT_PLUGINS = [
3182
3638
  "permissions",
3183
3639
  "logs",
3184
3640
  "agents",
3185
- "download",
3186
3641
  "capi",
3187
3642
  ];
3188
3643
  function registerDatabase(server) {
3189
- // Skip NoSQL database tools for international region (Singapore) as it doesn't support NoSQL DB
3190
- const region = server.cloudBaseOptions?.region || process.env.TCB_REGION;
3191
- if (!(0, tencent_cloud_js_1.isInternationalRegion)(region)) {
3644
+ // NoSQL 数据库按站点能力集合注册(国际站默认不支持 NoSQL)
3645
+ const { site } = (0, site_map_js_1.resolveSiteAndRegion)({
3646
+ site: server.cloudBaseOptions?.site,
3647
+ region: server.cloudBaseOptions?.region,
3648
+ });
3649
+ if (site_map_js_1.SITE_REGION_MAP[site].capabilities.noSql) {
3192
3650
  (0, databaseNoSQL_js_1.registerDatabaseTools)(server);
3193
3651
  }
3194
3652
  (0, dataModel_js_1.registerDataModelTools)(server);
@@ -3197,8 +3655,11 @@ function registerMysqlDatabase(server) {
3197
3655
  (0, databaseSQL_js_1.registerSQLDatabaseTools)(server);
3198
3656
  }
3199
3657
  function registerNoSQLDatabase(server) {
3200
- const region = server.cloudBaseOptions?.region || process.env.TCB_REGION;
3201
- if (!(0, tencent_cloud_js_1.isInternationalRegion)(region)) {
3658
+ const { site } = (0, site_map_js_1.resolveSiteAndRegion)({
3659
+ site: server.cloudBaseOptions?.site,
3660
+ region: server.cloudBaseOptions?.region,
3661
+ });
3662
+ if (site_map_js_1.SITE_REGION_MAP[site].capabilities.noSql) {
3202
3663
  (0, databaseNoSQL_js_1.registerDatabaseTools)(server);
3203
3664
  }
3204
3665
  }
@@ -3217,7 +3678,6 @@ const AVAILABLE_PLUGINS = {
3217
3678
  storage: { name: "storage", register: storage_js_1.registerStorageTools },
3218
3679
  setup: { name: "setup", register: setup_js_1.registerSetupTools },
3219
3680
  rag: { name: "rag", register: rag_js_1.registerRagTools },
3220
- download: { name: "download", register: download_js_1.registerDownloadTools },
3221
3681
  gateway: { name: "gateway", register: gateway_js_1.registerGatewayTools },
3222
3682
  "app-auth": { name: "app-auth", register: app_auth_js_1.registerAppAuthTools },
3223
3683
  permissions: { name: "permissions", register: permissions_js_1.registerPermissionTools },
@@ -3404,26 +3864,6 @@ Object.defineProperty(exports, "reportToolkitLifecycle", ({ enumerable: true, ge
3404
3864
  Object.defineProperty(exports, "telemetryReporter", ({ enumerable: true, get: function () { return telemetry_js_1.telemetryReporter; } }));
3405
3865
 
3406
3866
 
3407
- /***/ }),
3408
-
3409
- /***/ 1454:
3410
- /***/ ((__unused_webpack_module, exports) => {
3411
-
3412
-
3413
- Object.defineProperty(exports, "__esModule", ({ value: true }));
3414
- exports.isInternationalRegion = void 0;
3415
- exports.isValidRegion = isValidRegion;
3416
- const REGION = {
3417
- SHANGHAI: 'ap-shanghai',
3418
- SINGAPORE: 'ap-singapore',
3419
- };
3420
- const isInternationalRegion = (region) => region === REGION.SINGAPORE;
3421
- exports.isInternationalRegion = isInternationalRegion;
3422
- function isValidRegion(region) {
3423
- return Object.values(REGION).includes(region);
3424
- }
3425
-
3426
-
3427
3867
  /***/ }),
3428
3868
 
3429
3869
  /***/ 1622:
@@ -4874,6 +5314,7 @@ const manager_node_1 = __importDefault(__webpack_require__(6665));
4874
5314
  const auth_js_1 = __webpack_require__(7291);
4875
5315
  const logger_js_1 = __webpack_require__(3039);
4876
5316
  const tool_result_js_1 = __webpack_require__(9835);
5317
+ const site_map_js_1 = __webpack_require__(8107);
4877
5318
  // Timeout for envId auto-resolution flow.
4878
5319
  // 10 minutes (600 seconds) - matches InteractiveServer timeout
4879
5320
  const ENV_ID_TIMEOUT = 600000;
@@ -4898,7 +5339,7 @@ function createManagerFromLoginState(loginState, region) {
4898
5339
  envId: loginState.envId,
4899
5340
  token: loginState.token,
4900
5341
  proxy: process.env.http_proxy,
4901
- region: region ?? process.env.TCB_REGION ?? 'ap-shanghai',
5342
+ region: region ?? (0, site_map_js_1.resolveSiteAndRegion)().region,
4902
5343
  });
4903
5344
  }
4904
5345
  async function listAvailableEnvCandidates(options) {
@@ -4926,7 +5367,7 @@ async function listAvailableEnvCandidates(options) {
4926
5367
  if (!loginState?.secretId || !loginState?.secretKey) {
4927
5368
  return [];
4928
5369
  }
4929
- const region = cloudBaseOptions?.region ?? process.env.TCB_REGION ?? 'ap-shanghai';
5370
+ const region = (0, site_map_js_1.resolveSiteAndRegion)(cloudBaseOptions ?? {}).region;
4930
5371
  cloudbase = createManagerFromLoginState(loginState, region);
4931
5372
  }
4932
5373
  try {
@@ -5127,7 +5568,10 @@ function getCachedEnvId() {
5127
5568
  const databaseInstanceIdCache = new Map();
5128
5569
  function buildDatabaseInstanceIdCacheKey(options) {
5129
5570
  const envId = options?.envId ?? options?.cloudBaseOptions?.envId ?? process.env.CLOUDBASE_ENV_ID ?? 'unknown';
5130
- const region = options?.region ?? options?.cloudBaseOptions?.region ?? process.env.TCB_REGION ?? 'ap-shanghai';
5571
+ const region = (0, site_map_js_1.resolveSiteAndRegion)({
5572
+ site: options?.cloudBaseOptions?.site,
5573
+ region: options?.region ?? options?.cloudBaseOptions?.region,
5574
+ }).region;
5131
5575
  return `${region}:${envId}`;
5132
5576
  }
5133
5577
  function resetDatabaseInstanceIdCache() {
@@ -5233,11 +5677,11 @@ async function getCloudBaseManager(options = {}) {
5233
5677
  });
5234
5678
  }
5235
5679
  try {
5236
- // Region priority: explicit option > env var > ap-shanghai default
5237
- const fallbackRegion = cloudBaseOptions?.region ?? process.env.TCB_REGION ?? 'ap-shanghai';
5680
+ // Region priority: explicit option > TCB_REGION/TCB_SITE env > .cloudbase/project.json > ap-shanghai default
5681
+ const { region: fallbackRegion, site } = (0, site_map_js_1.resolveSiteAndRegion)(cloudBaseOptions ?? {});
5238
5682
  const loginState = authStrategy === 'ensure'
5239
- ? await (0, auth_js_1.getLoginState)({ region: fallbackRegion })
5240
- : await (0, auth_js_1.peekLoginState)();
5683
+ ? await (0, auth_js_1.getLoginState)({ region: fallbackRegion, site })
5684
+ : await (0, auth_js_1.peekLoginState)({ region: fallbackRegion, site });
5241
5685
  if (!loginState) {
5242
5686
  const authState = await (0, auth_js_1.getAuthProgressState)();
5243
5687
  if (authState.status === 'PENDING') {
@@ -5285,7 +5729,7 @@ async function getCloudBaseManager(options = {}) {
5285
5729
  }
5286
5730
  // envId priority: explicit option > envManager cache > loginState.envId
5287
5731
  const resolvedEnvId = finalEnvId || loginEnvId;
5288
- // region 直接使用 fallbackRegion(来自 cloudBaseOptions?.region ?? TCB_REGION ?? 'ap-shanghai')
5732
+ // region 直接使用 fallbackRegion(resolveSiteAndRegion 统一解析)
5289
5733
  // 不再通过 DescribeEnvs 查询 region,以兼容 STS 临时密钥场景
5290
5734
  const region = fallbackRegion;
5291
5735
  const manager = new manager_node_1.default({
@@ -5317,8 +5761,8 @@ function createCloudBaseManagerWithOptions(cloudBaseOptions) {
5317
5761
  hasSecretKey: !!cloudBaseOptions.secretKey,
5318
5762
  hasToken: !!cloudBaseOptions.token,
5319
5763
  });
5320
- // Region priority: explicit option > env var > ap-shanghai default
5321
- const region = cloudBaseOptions.region ?? process.env.TCB_REGION ?? 'ap-shanghai';
5764
+ // Region priority: explicit option > TCB_REGION/TCB_SITE env > .cloudbase/project.json > ap-shanghai default
5765
+ const region = (0, site_map_js_1.resolveSiteAndRegion)(cloudBaseOptions).region;
5322
5766
  const manager = new manager_node_1.default({
5323
5767
  ...cloudBaseOptions,
5324
5768
  proxy: cloudBaseOptions.proxy || process.env.http_proxy,
@@ -5389,6 +5833,15 @@ const logger_js_1 = __webpack_require__(3039);
5389
5833
  const CATEGORY = "NoSQL database";
5390
5834
  const COLLECTION_READY_TIMEOUT_MS = 10000;
5391
5835
  const COLLECTION_READY_POLL_INTERVAL_MS = 500;
5836
+ /** Default page size for QueryRecords (unchanged; overrun tuning is a separate track). */
5837
+ const QUERY_RECORDS_DEFAULT_LIMIT = 100;
5838
+ /** Cloud API MgoLimit `lte` upper bound (aligned with ListTables / SDK limit docs). */
5839
+ const QUERY_RECORDS_MAX_LIMIT = 1000;
5840
+ const PROJECTION_EXAMPLE = '{"_id":1,"name":1,"createdAt":1}';
5841
+ const PROJECTION_EXCLUDE_EXAMPLE = '{"password":0}';
5842
+ const PROJECTION_GUIDANCE = `projection 必须是字段投影对象(或对应 JSON 字符串),值只能是 1/0/true/false。` +
5843
+ `合法示例:${PROJECTION_EXAMPLE}(只返回这些字段),或 ${PROJECTION_EXCLUDE_EXAMPLE}(排除字段)。` +
5844
+ `不要传字段名数组、逗号分隔字符串、查询条件,也不要在同一投影里混用包含(1/true)与排除(0/false)(_id 除外)。`;
5392
5845
  /** Convert object values to JSON strings for API calls */
5393
5846
  const toJSONString = (v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v;
5394
5847
  function delay(ms) {
@@ -5459,6 +5912,95 @@ function normalizeSortInput(sort) {
5459
5912
  }
5460
5913
  return JSON.stringify(parsed.map((item) => normalizeSortItem(item)));
5461
5914
  }
5915
+ function isProjectionFlag(value) {
5916
+ return value === 0 || value === 1 || value === true || value === false;
5917
+ }
5918
+ function normalizeProjectionInput(projection) {
5919
+ if (projection === undefined || projection === null) {
5920
+ return undefined;
5921
+ }
5922
+ let parsed = projection;
5923
+ if (typeof parsed === "string") {
5924
+ const trimmed = parsed.trim();
5925
+ if (!trimmed) {
5926
+ return undefined;
5927
+ }
5928
+ try {
5929
+ parsed = JSON.parse(trimmed);
5930
+ }
5931
+ catch {
5932
+ throw new Error(`projection 必须是合法 JSON 对象字符串。${PROJECTION_GUIDANCE}`);
5933
+ }
5934
+ }
5935
+ if (Array.isArray(parsed)) {
5936
+ throw new Error(`projection 不支持字段名数组。请改为对象,例如 ${PROJECTION_EXAMPLE}。${PROJECTION_GUIDANCE}`);
5937
+ }
5938
+ if (!parsed || typeof parsed !== "object") {
5939
+ throw new Error(`projection 必须是对象。请改为例如 ${PROJECTION_EXAMPLE}。${PROJECTION_GUIDANCE}`);
5940
+ }
5941
+ const entries = Object.entries(parsed);
5942
+ if (entries.length === 0) {
5943
+ return undefined;
5944
+ }
5945
+ let hasInclude = false;
5946
+ let hasExclude = false;
5947
+ const normalized = {};
5948
+ for (const [key, value] of entries) {
5949
+ if (typeof key !== "string" || key.trim() === "") {
5950
+ throw new Error(`projection 字段名必须是非空字符串。${PROJECTION_GUIDANCE}`);
5951
+ }
5952
+ if (!isProjectionFlag(value)) {
5953
+ throw new Error(`projection["${key}"] 的值非法(当前为 ${JSON.stringify(value)}),仅支持 1/0/true/false。` +
5954
+ `合法示例:${PROJECTION_EXAMPLE}。${PROJECTION_GUIDANCE}`);
5955
+ }
5956
+ const flag = value === true || value === 1 ? 1 : 0;
5957
+ normalized[key] = flag;
5958
+ if (key === "_id") {
5959
+ continue;
5960
+ }
5961
+ if (flag === 1) {
5962
+ hasInclude = true;
5963
+ }
5964
+ else {
5965
+ hasExclude = true;
5966
+ }
5967
+ }
5968
+ if (hasInclude && hasExclude) {
5969
+ throw new Error(`projection 不能同时混用包含(1/true)与排除(0/false)(_id 除外)。` +
5970
+ `请只保留一种模式,例如 ${PROJECTION_EXAMPLE} 或 ${PROJECTION_EXCLUDE_EXAMPLE}。`);
5971
+ }
5972
+ return JSON.stringify(normalized);
5973
+ }
5974
+ function normalizeQueryLimit(limit) {
5975
+ if (limit === undefined || limit === null) {
5976
+ return QUERY_RECORDS_DEFAULT_LIMIT;
5977
+ }
5978
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
5979
+ throw new Error(`limit 必须是数字,取值范围 1-${QUERY_RECORDS_MAX_LIMIT}(默认 ${QUERY_RECORDS_DEFAULT_LIMIT})。` +
5980
+ `超出上限时请用 offset 分页,例如 limit=${QUERY_RECORDS_MAX_LIMIT}, offset=0,再 offset+=limit。`);
5981
+ }
5982
+ if (!Number.isInteger(limit)) {
5983
+ throw new Error(`limit 必须是整数,取值范围 1-${QUERY_RECORDS_MAX_LIMIT}。当前值:${limit}`);
5984
+ }
5985
+ if (limit < 1 || limit > QUERY_RECORDS_MAX_LIMIT) {
5986
+ throw new Error(`limit 超出上限:当前 ${limit},允许范围 1-${QUERY_RECORDS_MAX_LIMIT}(Cloud API MgoLimit lte)。` +
5987
+ `请将 limit 调整为 ≤${QUERY_RECORDS_MAX_LIMIT},并用 offset 分页拉取更多数据` +
5988
+ `(例如 limit=${QUERY_RECORDS_MAX_LIMIT}, offset=0,下一页 offset=${QUERY_RECORDS_MAX_LIMIT})。`);
5989
+ }
5990
+ return limit;
5991
+ }
5992
+ function enhanceQueryRecordsError(error) {
5993
+ const message = error instanceof Error ? error.message : String(error ?? "");
5994
+ if (/Query projection.*illegal|projection entered in the request is illegal/i.test(message)) {
5995
+ throw new Error(`QueryRecords 投影非法:${message}。修正建议:${PROJECTION_GUIDANCE}`);
5996
+ }
5997
+ if (/MgoLimit.*lte|Field validation for 'MgoLimit' failed on the 'lte'/i.test(message)) {
5998
+ throw new Error(`QueryRecords MgoLimit 超限:${message}。` +
5999
+ `limit 最大为 ${QUERY_RECORDS_MAX_LIMIT},请缩小 limit 并用 offset 分页` +
6000
+ `(例如 limit=${QUERY_RECORDS_MAX_LIMIT}, offset=0)。`);
6001
+ }
6002
+ throw error instanceof Error ? error : new Error(message);
6003
+ }
5462
6004
  function withCollectionName(collectionName, payload) {
5463
6005
  return {
5464
6006
  ...payload,
@@ -5993,7 +6535,9 @@ deleteCollection: 删除集合`),
5993
6535
  // readNoSqlDatabaseContent
5994
6536
  server.registerTool?.("readNoSqlDatabaseContent", {
5995
6537
  title: "查询并获取 CloudBase NoSQL 数据库数据记录",
5996
- description: "查询 CloudBase NoSQL 数据库中的数据记录。支持按条件筛选、分页、排序,适用于管理端数据查询与运维。",
6538
+ description: "查询 CloudBase NoSQL 数据库中的数据记录。支持按条件筛选、分页、排序,适用于管理端数据查询与运维。" +
6539
+ `limit 默认 ${QUERY_RECORDS_DEFAULT_LIMIT}、最大 ${QUERY_RECORDS_MAX_LIMIT};超出请用 offset 分页。` +
6540
+ `projection 仅支持 { field: 1|0 } 对象(示例 ${PROJECTION_EXAMPLE}),不要传字段数组。`,
5997
6541
  inputSchema: {
5998
6542
  collectionName: zod_1.z.string().describe("集合名称"),
5999
6543
  instanceId: zod_1.z
@@ -6007,7 +6551,9 @@ deleteCollection: 删除集合`),
6007
6551
  projection: zod_1.z
6008
6552
  .union([zod_1.z.object({}).passthrough(), zod_1.z.string()])
6009
6553
  .optional()
6010
- .describe("返回字段投影(对象或字符串,推荐对象)"),
6554
+ .describe(`返回字段投影,仅支持对象或对应 JSON 字符串,值只能是 1/0/true/false。` +
6555
+ `合法示例:${PROJECTION_EXAMPLE}(包含)或 ${PROJECTION_EXCLUDE_EXAMPLE}(排除)。` +
6556
+ `不要传 ["name","age"] 这类字段数组,也不要混用包含与排除(_id 除外)。`),
6011
6557
  sort: zod_1.z
6012
6558
  .union([
6013
6559
  zod_1.z.array(zod_1.z
@@ -6020,7 +6566,11 @@ deleteCollection: 删除集合`),
6020
6566
  ])
6021
6567
  .optional()
6022
6568
  .describe('排序条件,仅支持数组 [{"key":"createdAt","direction":-1}] 或对应 JSON 字符串。'),
6023
- limit: zod_1.z.number().optional().describe("返回数量限制"),
6569
+ limit: zod_1.z
6570
+ .number()
6571
+ .optional()
6572
+ .describe(`返回数量限制,整数,范围 1-${QUERY_RECORDS_MAX_LIMIT},默认 ${QUERY_RECORDS_DEFAULT_LIMIT}。` +
6573
+ `超过 ${QUERY_RECORDS_MAX_LIMIT} 会被 Cloud API MgoLimit lte 校验拒绝;请用 offset 分页。`),
6024
6574
  offset: zod_1.z.number().optional().describe("跳过的记录数"),
6025
6575
  },
6026
6576
  annotations: {
@@ -6036,46 +6586,53 @@ deleteCollection: 删除集合`),
6036
6586
  durationMs: Date.now() - managerStartedAt,
6037
6587
  });
6038
6588
  const normalizedSort = normalizeSortInput(sort);
6039
- const result = await callNoSqlContentApi({
6040
- toolName: "readNoSqlDatabaseContent",
6041
- action: "QueryRecords",
6042
- collectionName,
6043
- cloudbase,
6044
- cloudBaseOptions,
6045
- instanceIdOverride: instanceId,
6046
- param: {
6047
- MgoQuery: toJSONString(query),
6048
- MgoProjection: toJSONString(projection),
6049
- MgoSort: normalizedSort,
6050
- MgoLimit: limit ?? 100,
6051
- MgoOffset: offset,
6052
- },
6053
- });
6054
- (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
6055
- const parseStartedAt = Date.now();
6056
- const documents = normalizeNoSqlDocuments(result.Data);
6057
- logNoSqlLatency("readNoSqlDatabaseContent", "parseResult", {
6058
- collectionName,
6059
- durationMs: Date.now() - parseStartedAt,
6060
- documentCount: documents.length,
6061
- });
6062
- return {
6063
- content: [
6064
- {
6065
- type: "text",
6066
- text: JSON.stringify(withCollectionName(collectionName, {
6067
- success: true,
6068
- requestId: result.RequestId,
6069
- data: documents,
6070
- total: typeof result.Pager?.Total === "number"
6071
- ? result.Pager.Total
6072
- : documents.length,
6073
- pager: result.Pager,
6074
- message: "文档查询成功",
6075
- }), null, 2),
6589
+ const normalizedProjection = normalizeProjectionInput(projection);
6590
+ const normalizedLimit = normalizeQueryLimit(limit);
6591
+ try {
6592
+ const result = await callNoSqlContentApi({
6593
+ toolName: "readNoSqlDatabaseContent",
6594
+ action: "QueryRecords",
6595
+ collectionName,
6596
+ cloudbase,
6597
+ cloudBaseOptions,
6598
+ instanceIdOverride: instanceId,
6599
+ param: {
6600
+ MgoQuery: toJSONString(query),
6601
+ MgoProjection: normalizedProjection,
6602
+ MgoSort: normalizedSort,
6603
+ MgoLimit: normalizedLimit,
6604
+ MgoOffset: offset,
6076
6605
  },
6077
- ],
6078
- };
6606
+ });
6607
+ (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
6608
+ const parseStartedAt = Date.now();
6609
+ const documents = normalizeNoSqlDocuments(result.Data);
6610
+ logNoSqlLatency("readNoSqlDatabaseContent", "parseResult", {
6611
+ collectionName,
6612
+ durationMs: Date.now() - parseStartedAt,
6613
+ documentCount: documents.length,
6614
+ });
6615
+ return {
6616
+ content: [
6617
+ {
6618
+ type: "text",
6619
+ text: JSON.stringify(withCollectionName(collectionName, {
6620
+ success: true,
6621
+ requestId: result.RequestId,
6622
+ data: documents,
6623
+ total: typeof result.Pager?.Total === "number"
6624
+ ? result.Pager.Total
6625
+ : documents.length,
6626
+ pager: result.Pager,
6627
+ message: "文档查询成功",
6628
+ }), null, 2),
6629
+ },
6630
+ ],
6631
+ };
6632
+ }
6633
+ catch (error) {
6634
+ enhanceQueryRecordsError(error);
6635
+ }
6079
6636
  });
6080
6637
  // writeNoSqlDatabaseContent
6081
6638
  server.registerTool?.("writeNoSqlDatabaseContent", {
@@ -8199,7 +8756,10 @@ function registerGatewayTools(server) {
8199
8756
  };
8200
8757
  const pickUsableDefaultDomain = (domains) => {
8201
8758
  const candidates = domains.filter((item) => item.IsDefault === true && item.Domain);
8202
- return (candidates.find((item) => item.Enable !== false && item.Status === "SUCCESS") ??
8759
+ return (candidates.find((item) => item.Enable !== false &&
8760
+ // Platform may return Success/success; normalize before matching.
8761
+ typeof item.Status === "string" &&
8762
+ item.Status.toLowerCase() === "success") ??
8203
8763
  candidates.find((item) => item.Enable !== false) ??
8204
8764
  candidates[0]);
8205
8765
  };
@@ -9607,8 +10167,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9607
10167
  return (mod && mod.__esModule) ? mod : { "default": mod };
9608
10168
  };
9609
10169
  Object.defineProperty(exports, "__esModule", ({ value: true }));
9610
- exports.CLOUDRUN_ACCESS_TYPES = exports.CLOUDRUN_SERVICE_TYPES = void 0;
10170
+ exports.CLOUDRUN_ENV_TYPE = exports.CLOUDRUN_DEPLOY_REGISTRATION_INTERVAL_MS = exports.CLOUDRUN_DEPLOY_REGISTRATION_MAX_WAIT_MS = exports.CLOUDRUN_PACKAGE_TYPES = exports.CLOUDRUN_ACCESS_TYPES = exports.CLOUDRUN_SERVICE_TYPES = void 0;
10171
+ exports.buildManageCloudRunErrorMessage = buildManageCloudRunErrorMessage;
10172
+ exports.isValidCloudRunBuildId = isValidCloudRunBuildId;
10173
+ exports.isValidCloudRunRunId = isValidCloudRunRunId;
10174
+ exports.waitForCloudRunDeployRegistration = waitForCloudRunDeployRegistration;
10175
+ exports.buildCloudRunDeployNextStep = buildCloudRunDeployNextStep;
10176
+ exports.buildCloudRunDeployProgressHint = buildCloudRunDeployProgressHint;
10177
+ exports.queryCloudRunEnvStatus = queryCloudRunEnvStatus;
10178
+ exports.describeCloudRunEnvStatus = describeCloudRunEnvStatus;
10179
+ exports.extractEnvBaseInfoVpc = extractEnvBaseInfoVpc;
10180
+ exports.resolveCloudRunDeployVpcInfo = resolveCloudRunDeployVpcInfo;
10181
+ exports.buildCreateCloudRunEnvParam = buildCreateCloudRunEnvParam;
10182
+ exports.ensureCloudRunEnvInitialized = ensureCloudRunEnvInitialized;
9611
10183
  exports.detectCloudRunDbNetworkRisk = detectCloudRunDbNetworkRisk;
10184
+ exports.extractCloudRunImageInfo = extractCloudRunImageInfo;
9612
10185
  exports.registerCloudRunTools = registerCloudRunTools;
9613
10186
  const child_process_1 = __webpack_require__(5814);
9614
10187
  const fs_1 = __importDefault(__webpack_require__(4421));
@@ -9618,29 +10191,44 @@ const cloudbase_manager_js_1 = __webpack_require__(3431);
9618
10191
  const logger_js_1 = __webpack_require__(3039);
9619
10192
  const gateway_access_urls_js_1 = __webpack_require__(2435);
9620
10193
  const notification_js_1 = __webpack_require__(2720);
10194
+ const capi_js_1 = __webpack_require__(738);
9621
10195
  const cloudrun_config_js_1 = __webpack_require__(8266);
9622
10196
  // CloudRun service types
9623
10197
  exports.CLOUDRUN_SERVICE_TYPES = ['function', 'container'];
9624
10198
  // CloudRun access types
9625
10199
  exports.CLOUDRUN_ACCESS_TYPES = ['OA', 'PUBLIC', 'MINIAPP', 'VPC'];
10200
+ // CloudRun env package types (CreateCloudRunEnv.PackageType)
10201
+ exports.CLOUDRUN_PACKAGE_TYPES = ['Trial', 'Standard', 'Professional', 'Enterprise'];
9626
10202
  // Input schema for queryCloudRun tool
9627
10203
  const queryCloudRunInputSchema = {
9628
- action: zod_1.z.enum(['list', 'detail', 'templates', 'getDeployLog']).describe('查询操作类型:list=获取云托管服务列表(支持分页和筛选),detail=查询指定服务的详细信息(包含服务配置和最新部署状态),templates=获取可用的项目模板列表(用于初始化新项目),getDeployLog=获取指定服务最近一次或指定构建的部署日志'),
10204
+ action: zod_1.z.enum(['list', 'detail', 'templates', 'getDeployLog', 'getProcessLog', 'getDeployRecords', 'envStatus']).describe('查询操作类型:list=获取云托管服务列表(支持分页和筛选),detail=查询指定服务的详细信息(包含服务配置和最新部署状态),templates=获取可用的项目模板列表(用于初始化新项目),getDeployLog=获取构建日志(仅云端源码构建有意义,走 CODING/DescribeCloudRunBuildLog;已有镜像部署无构建过程;未登录 CODING 的账号会报错),getProcessLog=获取运行日志(部署阶段步骤+容器启动/运行日志,走 tcbr/DescribeCloudRunProcessLog;镜像部署与源码构建均可用,不依赖 CODING;RunId 来自 detail/getDeployRecords 的 latestDeploy.RunId),getDeployRecords=获取指定服务的部署记录列表(按部署时间倒序,含 BuildId/RunId/FlowRatio/Status 等字段,用于查看历史发布与回滚上下文),envStatus=查询当前环境云托管是否已开通及开通状态(Status=creating开通中/normal已开通),用于initEnv之后轮询进度或deploy之前确认环境是否就绪'),
9629
10205
  // List operation parameters
9630
10206
  pageSize: zod_1.z.number().min(1).max(100).optional().default(10).describe('分页大小,控制每页返回的服务数量。取值范围:1-100,默认值:10。建议根据网络性能和显示需求调整'),
9631
10207
  pageNum: zod_1.z.number().min(1).optional().default(1).describe('页码,用于分页查询。从1开始,默认值:1。配合pageSize使用可实现分页浏览'),
9632
10208
  serverName: zod_1.z.string().optional().describe('服务名称筛选条件,支持模糊匹配。例如:输入"test"可匹配"test-service"、"my-test-app"等服务名称。留空则查询所有服务'),
9633
10209
  serverType: zod_1.z.enum(exports.CLOUDRUN_SERVICE_TYPES).optional().describe('服务类型筛选条件:function=函数型云托管(仅支持Node.js,有特殊的开发要求和限制,适合简单的API服务),container=容器型服务(推荐使用,支持任意语言和框架如Java/Go/Python/PHP/.NET等,适合大多数应用场景)'),
10210
+ envId: zod_1.z.string().optional().describe('环境 ID(action=envStatus 时使用;不传则使用当前配置的环境)。格式如 env-xxxxxx'),
9634
10211
  // Detail and log operation parameters
9635
- detailServerName: zod_1.z.string().optional().describe('要查询详细信息或部署日志的服务名称。当action为detail或getDeployLog时建议提供,必须是已存在的服务名称。可通过list操作获取可用的服务名称列表'),
9636
- buildId: zod_1.z.number().optional().describe('构建ID,仅在action=getDeployLog时使用。不传时默认返回最近一次部署的构建日志'),
10212
+ detailServerName: zod_1.z.string().optional().describe('要查询详细信息、部署记录、构建日志或运行日志的服务名称。当action为detail、getDeployLog、getProcessLog或getDeployRecords时建议提供,必须是已存在的服务名称。可通过list操作获取可用的服务名称列表'),
10213
+ buildId: zod_1.z.number().optional().describe('构建ID,仅在action=getDeployLog时使用(构建日志,仅云端源码构建)。不传时默认返回最近一次部署的构建日志'),
10214
+ runId: zod_1.z.string().optional().describe('运行ID(RunId),仅在action=getProcessLog时使用。不传时默认取该服务最近一次部署记录的 RunId(与 detail/getDeployRecords 的 latestDeploy.RunId 同源)。镜像部署与源码构建均可查询运行日志'),
9637
10215
  };
9638
10216
  // Input schema for manageCloudRun tool
9639
10217
  const ManageCloudRunInputSchema = {
9640
- action: zod_1.z.enum(['init', 'download', 'run', 'deploy', 'delete', 'createAgent', 'updateConfig']).describe('云托管服务管理操作类型:init=从模板初始化新的云托管项目代码(在targetPath目录下创建以serverName命名的子目录,支持多种语言和框架模板),download=从云端下载现有服务的代码到本地进行开发,run=在本地运行函数型云托管服务(用于开发和调试,仅支持函数型服务),deploy=将本地代码部署到云端云托管服务(支持函数型和容器型;已存在服务会 Read-Merge-Write 保留远程 VpcConf/EnvParams/OpenAccessTypes),updateConfig=仅更新服务配置不重新上传代码(对齐控制台服务设置,走 SubmitServerConfigChangeDiff;不需要 targetPath),delete=删除指定的云托管服务(不可恢复,需要确认),createAgent=创建函数型Agent(基于函数型云托管开发AI智能体)'),
9641
- serverName: zod_1.z.string().describe('云托管服务名称,用于标识和管理服务。命名规则:支持大小写字母、数字、连字符和下划线,必须以字母开头,长度3-45个字符。在init操作中会作为在targetPath下创建的子目录名,在其他操作中作为目标服务名'),
10218
+ action: zod_1.z.enum(['init', 'download', 'run', 'deploy', 'delete', 'createAgent', 'updateConfig', 'initEnv', 'traffic']).describe('云托管服务管理操作类型:init=从模板初始化新的云托管项目代码(在targetPath目录下创建以serverName命名的子目录,支持多种语言和框架模板),download=从云端下载现有服务的代码到本地进行开发,run=在本地运行函数型云托管服务(用于开发和调试,仅支持函数型服务),deploy=触发部署并轻量等待任务注册(不会 hang 等完整构建)。源码构建(targetPath)返回 buildId,用 getDeployLog 轮询后再 getProcessLog;已有镜像部署(imageUrl,DeployType=image,BuildId 常为 0)跳过 getDeployLog,用 detail/getDeployRecords 取 RunId 后 getProcessLog。传 imageUrl 时 targetPath 可省略;已存在服务会 Read-Merge-Write 保留远程 VpcConf/EnvParams/OpenAccessTypes),updateConfig=仅更新服务配置不重新上传代码(对齐控制台服务设置,走 SubmitServerConfigChangeDiff;不需要 targetPath),delete=删除指定的云托管服务(不可恢复,需要确认),createAgent=创建函数型Agent(基于函数型云托管开发AI智能体),initEnv=开通当前环境的云托管(异步创建云托管环境,幂等:已开通直接返回;适合新环境首次部署前使用),traffic=流量管理与灰度发布(set=调整稳定版/灰度版流量比例,promote=将灰度版本升级为全量,rollback=回滚到上一个稳定版本;对应 tcb cloudrun traffic 命令)'),
10219
+ serverName: zod_1.z.string().describe('云托管服务名称,用于标识和管理服务。命名规则:支持大小写字母、数字、连字符和下划线,必须以字母开头,长度3-45个字符。在init操作中会作为在targetPath下创建的子目录名,在其他操作中作为目标服务名。initEnv 操作不需要此参数'),
10220
+ // Traffic management operation parameters (action=traffic)
10221
+ trafficOp: zod_1.z.enum(['set', 'promote', 'rollback']).optional().describe('流量管理子操作(action=traffic 时使用):set=调整灰度流量比例(需先部署新版本至灰度,通过 stablePercent/canaryPercent 设置稳定版与灰度版流量比例,两者之和必须等于100);promote=将灰度版本全量发布(灰度版本流量置为100%并关闭灰度发布,等价于 tcb cloudrun traffic promote);rollback=回滚到上一个稳定版本(停止当前灰度/发布中的版本,回到稳定版本,等价于 tcb cloudrun traffic rollback)'),
10222
+ stablePercent: zod_1.z.number().min(0).max(100).optional().describe('稳定版本流量比例(trafficOp=set 时使用),取值范围0-100。与 canaryPercent 之和必须等于100。例如希望 90% 流量打到稳定版、10% 打到灰度版,则 stablePercent=90, canaryPercent=10'),
10223
+ canaryPercent: zod_1.z.number().min(0).max(100).optional().describe('灰度版本流量比例(trafficOp=set 时使用),取值范围0-100。与 stablePercent 之和必须等于100。例如希望 90% 流量打到稳定版、10% 打到灰度版,则 stablePercent=90, canaryPercent=10'),
10224
+ // InitEnv operation parameters
10225
+ envId: zod_1.z.string().optional().describe('环境 ID(action=initEnv 时使用;不传则使用当前配置的环境)。格式如 env-xxxxxx'),
10226
+ packageType: zod_1.z.enum(exports.CLOUDRUN_PACKAGE_TYPES).optional().default('Trial').describe('云托管环境套餐类型(action=initEnv 时使用):Trial=试用,Standard=标准,Professional=专业,Enterprise=企业。默认 Trial'),
10227
+ vpcId: zod_1.z.string().optional().describe('VPC 网络 ID(action=initEnv 时可选)。当平台拒绝系统创建网络时必填,格式如 vpc-xxxxxxxx。与 subnetIds 一起透传给 CreateCloudRunEnv 的 VpcId/SubNetIds。多数场景可不传(由系统创建网络)'),
10228
+ subnetIds: zod_1.z.array(zod_1.z.string()).optional().describe('子网 ID 列表(action=initEnv 时可选)。当需指定自有 VPC 时必填,如 ["subnet-xxxxxxxx"]。与 vpcId 一起透传给 CreateCloudRunEnv 的 SubNetIds'),
9642
10229
  // Deploy operation parameters
9643
- targetPath: zod_1.z.string().optional().describe('本地代码路径,必须是绝对路径。在deploy操作中指定要部署的代码目录,在download操作中指定下载目标目录,在init操作中指定云托管服务的上级目录(会在该目录下创建以serverName命名的子目录)。updateConfig 不需要此参数。建议约定:项目根目录下的cloudrun/目录,例如:/Users/username/projects/my-project/cloudrun'),
10230
+ targetPath: zod_1.z.string().optional().describe('本地代码路径,必须是绝对路径。在deploy操作中指定要部署的代码目录,在download操作中指定下载目标目录,在init操作中指定云托管服务的上级目录(会在该目录下创建以serverName命名的子目录)。updateConfig 不需要此参数。建议约定:项目根目录下的cloudrun/目录,例如:/Users/username/projects/my-project/cloudrun。使用 imageUrl 部署已有镜像时此参数可省略。注意:本地有源码目录不等于必须走源码构建;若用户指定镜像请优先传 imageUrl,不要仅因存在 targetPath 就回退到源码构建'),
10231
+ imageUrl: zod_1.z.string().optional().describe('已有镜像部署(action=deploy 时使用):直接指定容器镜像地址,如 ccr.ccs.tencentyun.com/ns/img:v1 或公网 registry 地址。传入后走 DeployType="image"(容器型)部署,无需本地源码目录(targetPath 可省略)。支持:1) 公网匿名可拉取的镜像直填地址;2) 私有/需登录的镜像(如 ghcr.io)需先在本地 docker pull → docker tag/push 到腾讯云 CCR → 填入 CCR 地址。不传则维持源码构建(本地代码打包上传)。约束:若用户明确提到使用某个镜像、或无需重新构建代码,则必须传 imageUrl 走镜像部署,不要回退到源码构建。注意:无论哪种部署方式,环境都需先开通云托管(未开通时先调用 initEnv,Status=normal 后再部署)'),
9644
10232
  envParamsReplaceAll: zod_1.z.boolean().optional().default(false).describe('EnvParams 合并策略(deploy / updateConfig):false(默认)= 与远程按 key 合并(输入覆盖同名 key,远程其余 key 保留);true= 用输入 EnvParams 整包替换远程。仅当显式传入 EnvParams 时生效'),
9645
10233
  serverConfig: zod_1.z.object({
9646
10234
  OpenAccessTypes: zod_1.z.array(zod_1.z.enum(exports.CLOUDRUN_ACCESS_TYPES)).optional().describe('公网访问类型配置,控制服务的访问权限:OA=办公网访问,PUBLIC=公网访问(默认,可通过HTTPS域名访问),MINIAPP=小程序访问,VPC=VPC访问(仅同VPC内可访问)。可配置多个类型'),
@@ -9661,7 +10249,7 @@ const ManageCloudRunInputSchema = {
9661
10249
  InternalDomain: zod_1.z.string().optional().describe('内网域名配置,用于配置服务的内网访问域名。仅在启用内网访问时有效'),
9662
10250
  EntryPoint: zod_1.z.array(zod_1.z.string()).optional().describe('Dockerfile EntryPoint参数配置,仅容器型服务需要。指定容器启动时的入口程序数组,如["node","app.js"]'),
9663
10251
  Cmd: zod_1.z.array(zod_1.z.string()).optional().describe('Dockerfile Cmd参数配置,仅容器型服务需要。指定容器启动时的默认命令数组,如["npm","start"]'),
9664
- InitialDelaySeconds: zod_1.z.number().min(0).optional().describe('延迟检测时间(秒),用于配置服务启动后的健康检查延迟。在此期间内不会将请求路由到该实例,适用于启动时间较长的服务'),
10252
+ InitialDelaySeconds: zod_1.z.number().min(0).optional().describe('端口健康检查初始延迟(秒)。部署完成后先等待 N 秒才开始端口探测,之后约每 5s 检查一次、连续约 30 次;30 次全失败才判定部署失败(约 150s 探测窗口),不是「N 秒后立即失败」。启动耗时长的应用建议调到 60–120'),
9665
10253
  LogType: zod_1.z.string().optional().describe('日志类型配置,指定服务的日志收集类型。影响日志的采集方式和存储格式'),
9666
10254
  LogSetId: zod_1.z.string().optional().describe('CLS日志集ID配置,指定日志服务(CLS)的日志集ID。需要先开通CLS日志服务'),
9667
10255
  LogTopicId: zod_1.z.string().optional().describe('CLS日志主题ID配置,指定日志服务(CLS)的日志主题ID。需要先开通CLS日志服务'),
@@ -9775,40 +10363,404 @@ function validateAndNormalizePath(inputPath) {
9775
10363
  function buildManageCloudRunErrorMessage(action, serverName, error) {
9776
10364
  const baseMessage = error instanceof Error ? error.message : String(error);
9777
10365
  const suggestions = [];
10366
+ if ((0, capi_js_1.isCamAuthError)(baseMessage)) {
10367
+ suggestions.push((0, capi_js_1.buildCamAuthGuidance)());
10368
+ }
9778
10369
  if (/已有部署发布任务运行中|部署发布任务运行中/i.test(baseMessage)) {
9779
10370
  suggestions.push(`服务 \`${serverName}\` 当前已有部署任务在执行,请等待现有任务完成后再重试。`);
9780
10371
  suggestions.push("如果你确认要覆盖当前流程,可在合适时机使用 `force=true` 再次发起。");
9781
10372
  }
10373
+ if (/云托管资源未开通|无法使用系统创建网络|VpcInfo/i.test(baseMessage)) {
10374
+ suggestions.push("CreateCloudRunServer 需要有效 VPC:请传 serverConfig.VpcConf(VpcId+SubnetId)," +
10375
+ "或在 initEnv 时传入 vpcId/subnetIds(环境开通后 deploy 会自动从 EnvBaseInfo 回填)。" +
10376
+ "若平台拒绝系统创建网络,必须指定上海地域 VPC。");
10377
+ }
9782
10378
  if (suggestions.length === 0) {
9783
10379
  suggestions.push("请检查服务状态、部署参数和目标目录后重试。");
9784
10380
  }
9785
10381
  return `[manageCloudRun/${action}] ${baseMessage}\n建议:${suggestions.join(" ")}`;
9786
10382
  }
9787
- const CLOUDRUN_DB_ENV_KEY_PATTERN = /^(DATABASE_URL|DB_HOST|DB_PORT|DB_USER|DB_PASSWORD|DB_NAME|MYSQL_|POSTGRES_|PGHOST|PGPORT|PGUSER|PGPASSWORD|PGDATABASE|PG_|REDIS_|MONGO_|MONGODB_|SQLALCHEMY_DATABASE_URI|SPRING_DATASOURCE_)/i;
9788
- const CLOUDRUN_DB_URL_PATTERN = /(mysql|mariadb|postgres|postgresql|mongodb(\+srv)?|redis|rediss):\/\//i;
9789
10383
  /**
9790
- * Detect likely TCP database/cache env usage without VpcConf.
9791
- * Soft signal for AI agents — does not block deploy.
10384
+ * Lightweight wait after manageCloudRun deploy: poll until the platform has
10385
+ * registered the manage task / assigned identifiers for follow-up log queries.
10386
+ * - source builds: prefer BuildId > 0 (getDeployLog)
10387
+ * - image deploys: prefer RunId (getProcessLog; BuildId is often 0)
10388
+ * Does NOT wait for the full build/deploy (that can take 5–30 minutes).
10389
+ * Aligns with tcb CLI's DescribeServerManageTask polling entrypoint, but with
10390
+ * a short timeout suitable for MCP tool calls.
9792
10391
  */
9793
- function detectCloudRunDbNetworkRisk(options) {
9794
- const vpcId = options.vpcConf?.VpcId?.trim();
9795
- const subnetId = options.vpcConf?.SubnetId?.trim();
9796
- if (vpcId && subnetId) {
9797
- return null;
9798
- }
9799
- const rawEnv = options.envParams?.trim();
9800
- if (!rawEnv) {
9801
- return null;
9802
- }
9803
- let parsed;
9804
- try {
9805
- const value = JSON.parse(rawEnv);
9806
- if (!value || typeof value !== "object" || Array.isArray(value)) {
9807
- return null;
10392
+ exports.CLOUDRUN_DEPLOY_REGISTRATION_MAX_WAIT_MS = 45_000;
10393
+ exports.CLOUDRUN_DEPLOY_REGISTRATION_INTERVAL_MS = 3_000;
10394
+ function sleepMs(ms) {
10395
+ return new Promise((resolve) => setTimeout(resolve, ms));
10396
+ }
10397
+ function isValidCloudRunBuildId(value) {
10398
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
10399
+ }
10400
+ function isValidCloudRunRunId(value) {
10401
+ return typeof value === "string" && value.trim().length > 0;
10402
+ }
10403
+ function extractServerManageTaskInfo(resp) {
10404
+ const data = (resp ?? {});
10405
+ const task = (data.Task ?? data.task ?? {});
10406
+ const rawId = task.Id ?? task.id;
10407
+ const rawStatus = task.Status ?? task.status;
10408
+ const taskId = typeof rawId === "number" && rawId > 0
10409
+ ? rawId
10410
+ : typeof rawId === "string" && /^\d+$/.test(rawId) && Number(rawId) > 0
10411
+ ? Number(rawId)
10412
+ : undefined;
10413
+ const taskStatus = typeof rawStatus === "string" && rawStatus.trim()
10414
+ ? rawStatus.trim()
10415
+ : undefined;
10416
+ return {
10417
+ ...(taskId !== undefined ? { taskId } : {}),
10418
+ ...(taskStatus ? { taskStatus } : {}),
10419
+ };
10420
+ }
10421
+ /**
10422
+ * Poll DescribeServerManageTask + getDeployRecords until the preferred
10423
+ * identifier appears:
10424
+ * - source (default): BuildId > 0 for getDeployLog
10425
+ * - image: RunId for getProcessLog (BuildId is often 0)
10426
+ * If only taskId is seen, keeps polling for the preferred id until maxWaitMs.
10427
+ */
10428
+ async function waitForCloudRunDeployRegistration(options) {
10429
+ const mode = options.mode ?? "source";
10430
+ const maxWaitMs = options.maxWaitMs ?? exports.CLOUDRUN_DEPLOY_REGISTRATION_MAX_WAIT_MS;
10431
+ const intervalMs = options.intervalMs ?? exports.CLOUDRUN_DEPLOY_REGISTRATION_INTERVAL_MS;
10432
+ const sleepFn = options.sleepFn ?? sleepMs;
10433
+ const startAt = Date.now();
10434
+ let lastTaskId;
10435
+ let lastTaskStatus;
10436
+ let lastBuildId;
10437
+ let lastRunId;
10438
+ // Immediate first probe, then interval retries until timeout.
10439
+ for (;;) {
10440
+ if (options.manager.commonService) {
10441
+ try {
10442
+ const resp = await options.manager
10443
+ .commonService("tcbr", "2022-02-17")
10444
+ .call({
10445
+ Action: "DescribeServerManageTask",
10446
+ Param: {
10447
+ EnvId: options.envId,
10448
+ ServerName: options.serverName,
10449
+ TaskId: lastTaskId ?? 0,
10450
+ },
10451
+ });
10452
+ const info = extractServerManageTaskInfo(resp);
10453
+ if (info.taskId !== undefined) {
10454
+ lastTaskId = info.taskId;
10455
+ }
10456
+ if (info.taskStatus) {
10457
+ lastTaskStatus = info.taskStatus;
10458
+ }
10459
+ }
10460
+ catch {
10461
+ // Task may not be queryable yet; retry until timeout.
10462
+ }
9808
10463
  }
9809
- parsed = value;
9810
- }
9811
- catch {
10464
+ if (typeof options.cloudrunService.getDeployRecords === "function") {
10465
+ try {
10466
+ const recordsResult = (await options.cloudrunService.getDeployRecords({
10467
+ serverName: options.serverName,
10468
+ }));
10469
+ const latest = recordsResult?.DeployRecords?.[0];
10470
+ if (isValidCloudRunBuildId(latest?.BuildId)) {
10471
+ lastBuildId = latest.BuildId;
10472
+ }
10473
+ if (isValidCloudRunRunId(latest?.RunId)) {
10474
+ lastRunId = latest.RunId.trim();
10475
+ }
10476
+ }
10477
+ catch {
10478
+ // Deploy record may lag behind task registration; retry.
10479
+ }
10480
+ }
10481
+ const preferredReady = mode === "image"
10482
+ ? isValidCloudRunRunId(lastRunId)
10483
+ : isValidCloudRunBuildId(lastBuildId);
10484
+ if (preferredReady) {
10485
+ return {
10486
+ registered: true,
10487
+ timedOut: false,
10488
+ taskId: lastTaskId,
10489
+ ...(isValidCloudRunBuildId(lastBuildId) ? { buildId: lastBuildId } : {}),
10490
+ ...(isValidCloudRunRunId(lastRunId) ? { runId: lastRunId } : {}),
10491
+ taskStatus: lastTaskStatus,
10492
+ waitMs: Date.now() - startAt,
10493
+ };
10494
+ }
10495
+ if (Date.now() - startAt >= maxWaitMs) {
10496
+ break;
10497
+ }
10498
+ await sleepFn(intervalMs);
10499
+ }
10500
+ const registered = isValidCloudRunBuildId(lastBuildId) ||
10501
+ isValidCloudRunRunId(lastRunId) ||
10502
+ (typeof lastTaskId === "number" && lastTaskId > 0);
10503
+ return {
10504
+ registered,
10505
+ timedOut: true,
10506
+ taskId: lastTaskId,
10507
+ ...(isValidCloudRunBuildId(lastBuildId) ? { buildId: lastBuildId } : {}),
10508
+ ...(isValidCloudRunRunId(lastRunId) ? { runId: lastRunId } : {}),
10509
+ taskStatus: lastTaskStatus,
10510
+ waitMs: Date.now() - startAt,
10511
+ };
10512
+ }
10513
+ /**
10514
+ * Build manageCloudRun(deploy) next_step by DeployType:
10515
+ * - source: getDeployLog (+ follow-up getProcessLog)
10516
+ * - image: skip getDeployLog; getProcessLog via RunId (or getDeployRecords first)
10517
+ */
10518
+ function buildCloudRunDeployNextStep(options) {
10519
+ const { deployType, serverName, registered } = options;
10520
+ if (deployType === "image") {
10521
+ if (isValidCloudRunRunId(options.runId)) {
10522
+ return {
10523
+ tool: "queryCloudRun",
10524
+ action: "getProcessLog",
10525
+ suggested_args: {
10526
+ action: "getProcessLog",
10527
+ detailServerName: serverName,
10528
+ runId: options.runId.trim(),
10529
+ },
10530
+ note: "Image deploy has no CODING build process; skip getDeployLog. Poll getProcessLog for deploy-step and container runtime logs.",
10531
+ };
10532
+ }
10533
+ return {
10534
+ tool: "queryCloudRun",
10535
+ action: "getDeployRecords",
10536
+ suggested_args: {
10537
+ action: "getDeployRecords",
10538
+ detailServerName: serverName,
10539
+ },
10540
+ note: registered
10541
+ ? "Image deploy: read latestDeploy.RunId, then queryCloudRun(action=\"getProcessLog\", runId=...). Skip getDeployLog."
10542
+ : "Image deploy registration still queueing; retry getDeployRecords for RunId, then getProcessLog. Skip getDeployLog.",
10543
+ };
10544
+ }
10545
+ if (isValidCloudRunBuildId(options.buildId)) {
10546
+ return {
10547
+ tool: "queryCloudRun",
10548
+ action: "getDeployLog",
10549
+ suggested_args: {
10550
+ action: "getDeployLog",
10551
+ detailServerName: serverName,
10552
+ buildId: options.buildId,
10553
+ },
10554
+ note: "Source build: poll getDeployLog for build progress, then getProcessLog (RunId from detail/getDeployRecords) for runtime/deploy-step logs.",
10555
+ };
10556
+ }
10557
+ return {
10558
+ tool: "queryCloudRun",
10559
+ action: "getDeployLog",
10560
+ suggested_args: {
10561
+ action: "getDeployLog",
10562
+ detailServerName: serverName,
10563
+ },
10564
+ note: registered
10565
+ ? "BuildId not yet available; omit buildId to use the latest deploy record. After build, use getProcessLog for runtime logs."
10566
+ : "Registration timed out; wait a moment then retry getDeployLog, or open consoleUrl. After build, use getProcessLog for runtime logs.",
10567
+ };
10568
+ }
10569
+ function buildCloudRunDeployProgressHint(options) {
10570
+ const { deployType, serverName, consoleUrl } = options;
10571
+ if (deployType === "image") {
10572
+ if (isValidCloudRunRunId(options.runId)) {
10573
+ return ` Image deploy: use queryCloudRun(action="getProcessLog", detailServerName="${serverName}", runId="${options.runId.trim()}") for runtime/deploy-step logs (skip getDeployLog).`;
10574
+ }
10575
+ if (options.registered) {
10576
+ return ` Image deploy registered${typeof options.taskId === "number" ? ` (taskId=${options.taskId})` : ""}; RunId not yet available — use queryCloudRun(action="getDeployRecords", detailServerName="${serverName}") then getProcessLog (skip getDeployLog), or check ${consoleUrl}.`;
10577
+ }
10578
+ return ` Image deploy registration timed out after ${Math.round(options.waitMs / 1000)}s — retry getDeployRecords for RunId then getProcessLog (skip getDeployLog), or check ${consoleUrl}.`;
10579
+ }
10580
+ if (isValidCloudRunBuildId(options.buildId)) {
10581
+ return ` Use queryCloudRun(action="getDeployLog", detailServerName="${serverName}", buildId=${options.buildId}) to poll build progress, then getProcessLog for runtime logs.`;
10582
+ }
10583
+ if (options.registered) {
10584
+ return ` Task registered (taskId=${options.taskId ?? "unknown"}); BuildId not yet available — retry queryCloudRun(action="getDeployLog", detailServerName="${serverName}") shortly, then getProcessLog, or check ${consoleUrl}.`;
10585
+ }
10586
+ return ` Build registration timed out after ${Math.round(options.waitMs / 1000)}s — deployment may still be queueing. Check ${consoleUrl} or retry queryCloudRun(action="getDeployLog", detailServerName="${serverName}") later.`;
10587
+ }
10588
+ const CLOUDRUN_DB_ENV_KEY_PATTERN = /^(DATABASE_URL|DB_HOST|DB_PORT|DB_USER|DB_PASSWORD|DB_NAME|MYSQL_|POSTGRES_|PGHOST|PGPORT|PGUSER|PGPASSWORD|PGDATABASE|PG_|REDIS_|MONGO_|MONGODB_|SQLALCHEMY_DATABASE_URI|SPRING_DATASOURCE_)/i;
10589
+ const CLOUDRUN_DB_URL_PATTERN = /(mysql|mariadb|postgres|postgresql|mongodb(\+srv)?|redis|rediss):\/\//i;
10590
+ async function queryCloudRunEnvStatus(options) {
10591
+ const manager = await (0, cloudbase_manager_js_1.getCloudBaseManager)({ cloudBaseOptions: options.cloudBaseOptions });
10592
+ if (!manager?.commonService) {
10593
+ throw new Error("Current CloudBase Manager does not support commonService; cannot query CloudRun env status.");
10594
+ }
10595
+ return describeCloudRunEnvStatus(manager, options.envId);
10596
+ }
10597
+ /**
10598
+ * 核心实现:用已获取的 manager 查询云托管开通状态(供 initEnv/envStatus 及
10599
+ * ensureCloudRunEnvInitialized 复用,避免重复 getCloudBaseManager)。
10600
+ */
10601
+ async function describeCloudRunEnvStatus(manager, envId) {
10602
+ const result = await manager
10603
+ .commonService("tcbr", "2022-02-17")
10604
+ .call({
10605
+ Action: "DescribeEnvBaseInfo",
10606
+ Param: { EnvId: envId },
10607
+ });
10608
+ const data = (result ?? {});
10609
+ const baseInfo = (data.EnvBaseInfo ?? {}) ?? {};
10610
+ if (data.IsExist !== true) {
10611
+ return { isExist: false, status: "unopened", baseInfo };
10612
+ }
10613
+ const rawStatus = typeof baseInfo.Status === "string" ? baseInfo.Status : "";
10614
+ // Platform may return NORMAL/CREATING (uppercase); normalize before matching.
10615
+ const normalizedStatus = rawStatus.toLowerCase();
10616
+ const status = normalizedStatus === "creating" || normalizedStatus === "normal"
10617
+ ? normalizedStatus
10618
+ : "unknown";
10619
+ return { isExist: true, status, baseInfo };
10620
+ }
10621
+ /** CloudRun EnvType for CreateCloudRunEnv (tcbr DescribeEnvBaseInfo enum). */
10622
+ exports.CLOUDRUN_ENV_TYPE = "tcbr";
10623
+ /**
10624
+ * Extract VPC binding from DescribeEnvBaseInfo.EnvBaseInfo when the env was
10625
+ * opened with an explicit VPC.
10626
+ */
10627
+ function extractEnvBaseInfoVpc(baseInfo) {
10628
+ if (!baseInfo) {
10629
+ return undefined;
10630
+ }
10631
+ const vpcId = typeof baseInfo.VpcId === "string" ? baseInfo.VpcId.trim() : "";
10632
+ const rawSubnets = baseInfo.SubNetIds ?? baseInfo.SubnetIds;
10633
+ const subnetIds = Array.isArray(rawSubnets)
10634
+ ? rawSubnets
10635
+ .filter((item) => typeof item === "string" && item.trim().length > 0)
10636
+ .map((item) => item.trim())
10637
+ : [];
10638
+ if (!vpcId || subnetIds.length === 0) {
10639
+ return undefined;
10640
+ }
10641
+ return { VpcId: vpcId, SubnetIds: subnetIds };
10642
+ }
10643
+ /**
10644
+ * Resolve CreateCloudRunServer.VpcInfo: prefer explicit serverConfig.VpcConf,
10645
+ * otherwise fall back to env-level VPC from DescribeEnvBaseInfo.
10646
+ */
10647
+ function resolveCloudRunDeployVpcInfo(options) {
10648
+ const conf = options.vpcConf;
10649
+ if (conf?.VpcId?.trim() && conf?.SubnetId?.trim()) {
10650
+ return {
10651
+ VpcId: conf.VpcId.trim(),
10652
+ CreateType: 2,
10653
+ SubnetIds: [conf.SubnetId.trim()],
10654
+ };
10655
+ }
10656
+ const fromEnv = extractEnvBaseInfoVpc(options.envBaseInfo);
10657
+ if (!fromEnv) {
10658
+ return undefined;
10659
+ }
10660
+ return {
10661
+ VpcId: fromEnv.VpcId,
10662
+ CreateType: 2,
10663
+ SubnetIds: fromEnv.SubnetIds,
10664
+ };
10665
+ }
10666
+ /**
10667
+ * Build CreateCloudRunEnv Param, always including EnvType=tcbr.
10668
+ * Optional vpcId/subnetIds are used when the platform requires an explicit VPC.
10669
+ */
10670
+ function buildCreateCloudRunEnvParam(options) {
10671
+ const param = {
10672
+ EnvId: options.envId,
10673
+ PackageType: options.packageType,
10674
+ EnvType: exports.CLOUDRUN_ENV_TYPE,
10675
+ };
10676
+ const vpcId = options.vpcId?.trim();
10677
+ const subnetIds = (options.subnetIds ?? [])
10678
+ .filter((id) => typeof id === "string" && id.trim().length > 0)
10679
+ .map((id) => id.trim());
10680
+ if (vpcId) {
10681
+ param.VpcId = vpcId;
10682
+ }
10683
+ if (subnetIds.length > 0) {
10684
+ param.SubNetIds = subnetIds;
10685
+ }
10686
+ return param;
10687
+ }
10688
+ /**
10689
+ * 探测当前环境云托管(大租户)是否已初始化。
10690
+ *
10691
+ * 背景(2026-08-13 用户实测):新环境未调 CreateCloudRunEnv 初始化云托管时,
10692
+ * 直接 CreateCloudRunServer 会因"无大租户记录"被默认转入小租户,创建出小租户的
10693
+ * 服务和版本——这是错误路径。本函数在 deploy 创建新服务前用 tcbr DescribeEnvBaseInfo
10694
+ * 探测环境是否已开通云托管;未开通则抛错引导先初始化,而不是默默走到小租户路径。
10695
+ *
10696
+ * 实测(2026-08-13 真实凭据):tcbr 不存在 DescribeCloudRunEnv(单数)Action,调用
10697
+ * 恒返回 InvalidAction;正确的探测接口是 DescribeEnvBaseInfo。未开通云托管的环境返回
10698
+ * IsExist=false 且 EnvBaseInfo 为空结构(不抛错);已开通的环境(实测 ai-share-
10699
+ * d2guukyxybb63b206)返回 IsExist=true 且 EnvBaseInfo 含完整字段(Status="normal"、
10700
+ * PackageType/Region/EnvType/CreateTime 等已填充),两分支可明确区分。
10701
+ *
10702
+ * 双验证:DescribeCloudRunServers 未初始化与已初始化但无服务均返回 ServerList=[],
10703
+ * 无法区分,故不作为初始化判定依据。
10704
+ *
10705
+ * @returns 已初始化返回 true;探测到未初始化抛出带引导信息的 Error。
10706
+ */
10707
+ async function ensureCloudRunEnvInitialized(options) {
10708
+ const manager = await (0, cloudbase_manager_js_1.getCloudBaseManager)({ cloudBaseOptions: options.cloudBaseOptions });
10709
+ if (!manager?.commonService) {
10710
+ // 老 SDK 无 commonService 时退化为不拦截(保持既有行为,避免误伤)。
10711
+ return true;
10712
+ }
10713
+ try {
10714
+ const status = await describeCloudRunEnvStatus(manager, options.envId);
10715
+ if (!status.isExist) {
10716
+ throwCloudRunEnvNotInitialized(options.envId);
10717
+ }
10718
+ return true;
10719
+ }
10720
+ catch (error) {
10721
+ const msg = error instanceof Error ? error.message : String(error);
10722
+ // 错误兜底:未初始化/未开通类错误码。
10723
+ // 注意:裸 InvalidParameter 可能是普通参数错误,不拦截;仅当其带 Env/CloudRun
10724
+ // 上下文(疑似 "EnvironmentId not found / CloudRun Env 未开通")时才按未初始化处理。
10725
+ if (/ResourceNotFound|not.?initialized|未开通|未初始化/i.test(msg) ||
10726
+ /InvalidParameter.*(?:Env|CloudRun)/i.test(msg)) {
10727
+ throwCloudRunEnvNotInitialized(options.envId);
10728
+ }
10729
+ // 其他错误(网络/权限等)不拦截,让上层按原逻辑处理。
10730
+ return true;
10731
+ }
10732
+ }
10733
+ function throwCloudRunEnvNotInitialized(envId) {
10734
+ throw new Error(`当前环境(${envId})尚未初始化云托管(CloudRun Env)。` +
10735
+ `不能直接创建服务(CreateCloudRunServer 在无大租户记录时会默认创建到小租户,产生错误的小租户服务与版本)。\n` +
10736
+ `请先开通云托管环境,再重试部署:\n` +
10737
+ `- MCP:manageCloudRun(action="initEnv", envId="${envId}")(异步开通,幂等;开通完成后可用 queryCloudRun(action="envStatus", envId="${envId}") 查询 Status=normal)\n` +
10738
+ `- 或控制台:环境 → 云托管 → 开通(https://tcb.cloud.tencent.com/dev?envId=${envId}#/platform-run)\n` +
10739
+ `初始化完成后重新调用 manageCloudRun(action="deploy")。`);
10740
+ }
10741
+ /**
10742
+ * Detect likely TCP database/cache env usage without VpcConf.
10743
+ * Soft signal for AI agents — does not block deploy.
10744
+ */
10745
+ function detectCloudRunDbNetworkRisk(options) {
10746
+ const vpcId = options.vpcConf?.VpcId?.trim();
10747
+ const subnetId = options.vpcConf?.SubnetId?.trim();
10748
+ if (vpcId && subnetId) {
10749
+ return null;
10750
+ }
10751
+ const rawEnv = options.envParams?.trim();
10752
+ if (!rawEnv) {
10753
+ return null;
10754
+ }
10755
+ let parsed;
10756
+ try {
10757
+ const value = JSON.parse(rawEnv);
10758
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10759
+ return null;
10760
+ }
10761
+ parsed = value;
10762
+ }
10763
+ catch {
9812
10764
  // Non-JSON EnvParams still may embed connection URLs as plain text.
9813
10765
  if (!CLOUDRUN_DB_URL_PATTERN.test(rawEnv)) {
9814
10766
  return null;
@@ -9882,6 +10834,36 @@ function normalizeCloudRunDomainUrl(input) {
9882
10834
  ? raw
9883
10835
  : `https://${raw}`;
9884
10836
  }
10837
+ /**
10838
+ * 从服务详情 / 最新部署记录中提取镜像信息(镜像部署时才有)。
10839
+ * 镜像部署(DeployType=image)的服务详情与部署记录通常会携带 ImageUrl 等字段;
10840
+ * 源码构建的服务无此字段,返回 undefined 表示无镜像信息。
10841
+ */
10842
+ function extractCloudRunImageInfo(serviceDetail, latestDeploy) {
10843
+ const candidates = [
10844
+ latestDeploy,
10845
+ serviceDetail?.ServerConfig,
10846
+ serviceDetail?.BaseInfo,
10847
+ serviceDetail,
10848
+ ];
10849
+ for (const source of candidates) {
10850
+ if (!source || typeof source !== "object")
10851
+ continue;
10852
+ const imageUrl = source.ImageUrl ??
10853
+ source.imageUrl ??
10854
+ (typeof source.ImageInfo === "string" ? source.ImageInfo : undefined) ??
10855
+ (typeof source.ImageInfo?.ImageUrl === "string" ? source.ImageInfo.ImageUrl : undefined);
10856
+ if (typeof imageUrl === "string" && imageUrl.trim()) {
10857
+ const deployType = typeof source.DeployType === "string"
10858
+ ? source.DeployType
10859
+ : typeof source.deployType === "string"
10860
+ ? source.deployType
10861
+ : undefined;
10862
+ return { imageUrl: imageUrl.trim(), ...(deployType ? { deployType } : {}) };
10863
+ }
10864
+ }
10865
+ return undefined;
10866
+ }
9885
10867
  function resolveCloudRunFallbackAccess(details) {
9886
10868
  const custom = normalizeCloudRunDomainUrl(details?.BaseInfo?.CustomDomainName);
9887
10869
  if (custom)
@@ -9912,7 +10894,7 @@ function registerCloudRunTools(server) {
9912
10894
  // Tool 1: Get CloudRun service information (read operations)
9913
10895
  server.registerTool("queryCloudRun", {
9914
10896
  title: "查询 CloudRun 服务信息",
9915
- description: "查询云托管服务信息,支持获取服务列表、查询服务详情、获取可用模板列表和部署日志。返回的服务信息包括服务名称、状态、访问类型、配置详情以及最近部署上下文。",
10897
+ description: "查询云托管服务信息,支持获取服务列表、查询服务详情、获取可用模板列表、获取构建日志(getDeployLog,仅云端源码构建/依赖 CODING)、获取运行日志(getProcessLog,镜像与源码部署均可/不依赖 CODING)、获取部署记录以及查询环境云托管开通状态(envStatus)。返回的服务信息包括服务名称、状态、访问类型、配置详情以及最近部署上下文。",
9916
10898
  inputSchema: queryCloudRunInputSchema,
9917
10899
  annotations: {
9918
10900
  readOnlyHint: true,
@@ -10000,10 +10982,13 @@ function registerCloudRunTools(server) {
10000
10982
  if (!latestDeploy) {
10001
10983
  message = `Retrieved details for service '${serverName}'. No deploy records found yet.`;
10002
10984
  }
10003
- else if (typeof latestDeploy.Status === 'string' && latestDeploy.Status.includes('failed')) {
10004
- message = `Service '${serverName}' latest deploy failed. Please use queryCloudRun(action="getDeployLog") for details.`;
10985
+ else if (typeof latestDeploy.Status === "string" &&
10986
+ // Platform may return FAILED/CREATING (uppercase); normalize before matching.
10987
+ latestDeploy.Status.toLowerCase().includes("failed")) {
10988
+ message = `Service '${serverName}' latest deploy failed. Use queryCloudRun(action="getProcessLog") for runtime/deploy-step logs (RunId from latestDeploy); use getDeployLog only for cloud source-build logs (CODING).`;
10005
10989
  }
10006
- else if (typeof latestDeploy.Status === 'string' && latestDeploy.Status.includes('creating')) {
10990
+ else if (typeof latestDeploy.Status === "string" &&
10991
+ latestDeploy.Status.toLowerCase().includes("creating")) {
10007
10992
  message = `Service '${serverName}' latest deploy is still running. Please check again later or query the deploy log for progress.`;
10008
10993
  }
10009
10994
  else {
@@ -10024,6 +11009,10 @@ function registerCloudRunTools(server) {
10024
11009
  data: {
10025
11010
  service: result,
10026
11011
  latestDeploy,
11012
+ // 若最新部署记录带镜像信息(镜像部署),透出便于展示
11013
+ ...(extractCloudRunImageInfo(result, latestDeploy)
11014
+ ? { imageInfo: extractCloudRunImageInfo(result, latestDeploy) }
11015
+ : {}),
10027
11016
  ...(deployRecordsWarning ? { deployRecordsWarning } : {})
10028
11017
  },
10029
11018
  message
@@ -10082,6 +11071,9 @@ function registerCloudRunTools(server) {
10082
11071
  };
10083
11072
  }
10084
11073
  const buildId = input.buildId ?? latestDeploy.BuildId;
11074
+ // Build log (CODING / DescribeCloudRunBuildLog). Meaningful only for
11075
+ // cloud source builds; image deploys have no build process. Accounts
11076
+ // without a CODING user may fail here — use getProcessLog for runtime logs.
10085
11077
  const buildLogResult = await cloudrunService.getBuildLog({
10086
11078
  serverName,
10087
11079
  buildId,
@@ -10099,9 +11091,411 @@ function registerCloudRunTools(server) {
10099
11091
  processLogsWarning = error instanceof Error ? error.message : String(error);
10100
11092
  }
10101
11093
  }
10102
- const buildLogText = typeof buildLogResult?.Log?.Text === 'string' ? buildLogResult.Log.Text : '';
10103
- const processLogText = Array.isArray(processLogs) && processLogs.length > 0 ? normalizeProcessLogText(processLogs) : '';
10104
- const combinedLogText = [buildLogText, processLogText].filter(Boolean).join('\n');
11094
+ const buildLogText = typeof buildLogResult?.Log?.Text === 'string' ? buildLogResult.Log.Text : '';
11095
+ const processLogText = Array.isArray(processLogs) && processLogs.length > 0 ? normalizeProcessLogText(processLogs) : '';
11096
+ const combinedLogText = [buildLogText, processLogText].filter(Boolean).join('\n');
11097
+ return {
11098
+ content: [
11099
+ {
11100
+ type: "text",
11101
+ text: JSON.stringify({
11102
+ success: true,
11103
+ data: {
11104
+ buildId,
11105
+ deployRecord: latestDeploy,
11106
+ buildLog: buildLogResult?.Log || null,
11107
+ // Optional best-effort attach; prefer dedicated getProcessLog for runtime diagnosis
11108
+ processLogs,
11109
+ combinedLogText,
11110
+ ...(processLogsWarning ? { processLogsWarning } : {})
11111
+ },
11112
+ message: `Retrieved build log for service '${serverName}' (getDeployLog=构建日志; for 运行日志 use getProcessLog with RunId)`
11113
+ }, null, 2)
11114
+ }
11115
+ ]
11116
+ };
11117
+ }
11118
+ case 'getProcessLog': {
11119
+ const serverName = getCloudRunQueryServerName(input);
11120
+ if (!serverName && !input.runId?.trim()) {
11121
+ return {
11122
+ content: [
11123
+ {
11124
+ type: "text",
11125
+ text: JSON.stringify({
11126
+ success: false,
11127
+ error: "runId is required, or provide detailServerName/serverName to resolve latest RunId",
11128
+ message: "Pass runId from detail/getDeployRecords latestDeploy.RunId, or provide a service name to use the latest deploy RunId."
11129
+ }, null, 2)
11130
+ }
11131
+ ]
11132
+ };
11133
+ }
11134
+ let runId = input.runId?.trim() || "";
11135
+ let deployRecord = null;
11136
+ if (serverName) {
11137
+ const deployRecordsResult = await cloudrunService.getDeployRecords({ serverName });
11138
+ const deployRecords = Array.isArray(deployRecordsResult?.DeployRecords)
11139
+ ? deployRecordsResult.DeployRecords
11140
+ : [];
11141
+ deployRecord = deployRecords[0] ?? null;
11142
+ if (!runId) {
11143
+ runId = typeof deployRecord?.RunId === "string" ? deployRecord.RunId.trim() : "";
11144
+ }
11145
+ else if (deployRecord?.RunId && deployRecord.RunId !== runId) {
11146
+ // Keep latest deploy as context only; explicit runId wins
11147
+ }
11148
+ }
11149
+ if (!runId) {
11150
+ return {
11151
+ content: [
11152
+ {
11153
+ type: "text",
11154
+ text: JSON.stringify({
11155
+ success: false,
11156
+ error: serverName
11157
+ ? `Service '${serverName}' has no RunId on the latest deploy record.`
11158
+ : "runId is required for getProcessLog action",
11159
+ message: "Deploy the service first, then read RunId from queryCloudRun(action=\"detail\") or getDeployRecords (latestDeploy.RunId)."
11160
+ }, null, 2)
11161
+ }
11162
+ ]
11163
+ };
11164
+ }
11165
+ if (typeof cloudrunService.getProcessLog !== "function") {
11166
+ throw new Error("Current CloudBase Manager SDK does not support getProcessLog; please upgrade @cloudbase/manager-node.");
11167
+ }
11168
+ const processLogResult = await cloudrunService.getProcessLog({
11169
+ RunId: runId,
11170
+ });
11171
+ const processLogs = Array.isArray(processLogResult?.Logs)
11172
+ ? processLogResult.Logs
11173
+ : [];
11174
+ const processLogText = processLogs.length > 0
11175
+ ? normalizeProcessLogText(processLogs)
11176
+ : "";
11177
+ return {
11178
+ content: [
11179
+ {
11180
+ type: "text",
11181
+ text: JSON.stringify({
11182
+ success: true,
11183
+ data: {
11184
+ serverName: serverName || undefined,
11185
+ runId,
11186
+ deployRecord,
11187
+ processLogs,
11188
+ processLogText,
11189
+ requestId: processLogResult?.RequestId,
11190
+ },
11191
+ message: `Retrieved process/runtime log for RunId='${runId}' (getProcessLog=运行日志; getDeployLog=构建日志且仅云端构建/依赖 CODING)`
11192
+ }, null, 2)
11193
+ }
11194
+ ]
11195
+ };
11196
+ }
11197
+ case 'getDeployRecords': {
11198
+ const serverName = getCloudRunQueryServerName(input);
11199
+ if (!serverName) {
11200
+ return {
11201
+ content: [
11202
+ {
11203
+ type: "text",
11204
+ text: JSON.stringify({
11205
+ success: false,
11206
+ error: "detailServerName or serverName is required for getDeployRecords action",
11207
+ message: "Please provide detailServerName or serverName."
11208
+ }, null, 2)
11209
+ }
11210
+ ]
11211
+ };
11212
+ }
11213
+ const deployRecordsResult = await cloudrunService.getDeployRecords({ serverName });
11214
+ const deployRecords = Array.isArray(deployRecordsResult?.DeployRecords)
11215
+ ? deployRecordsResult.DeployRecords
11216
+ : [];
11217
+ return {
11218
+ content: [
11219
+ {
11220
+ type: "text",
11221
+ text: JSON.stringify({
11222
+ success: true,
11223
+ data: {
11224
+ serverName,
11225
+ deployRecords,
11226
+ // 部署记录按部署时间倒序(最新在前),首条为最近一次部署
11227
+ total: deployRecords.length,
11228
+ latestDeploy: deployRecords[0] ?? null
11229
+ },
11230
+ message: `Retrieved ${deployRecords.length} deploy records for service '${serverName}'`
11231
+ }, null, 2)
11232
+ }
11233
+ ]
11234
+ };
11235
+ }
11236
+ case 'envStatus': {
11237
+ const envId = input.envId?.trim() || (await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions));
11238
+ if (!manager.commonService) {
11239
+ throw new Error("Current CloudBase Manager does not support commonService; cannot query CloudRun env status.");
11240
+ }
11241
+ let status;
11242
+ try {
11243
+ status = await describeCloudRunEnvStatus(manager, envId);
11244
+ }
11245
+ catch (error) {
11246
+ const baseMessage = error instanceof Error ? error.message : String(error);
11247
+ if (/ResourceNotFound|not.?initialized|未开通|未初始化/i.test(baseMessage) ||
11248
+ /InvalidParameter.*(?:Env|CloudRun)/i.test(baseMessage)) {
11249
+ status = { isExist: false, status: "unopened", baseInfo: {} };
11250
+ }
11251
+ else {
11252
+ throw new Error(`[queryCloudRun/envStatus] ${baseMessage}`);
11253
+ }
11254
+ }
11255
+ let message;
11256
+ if (!status.isExist) {
11257
+ message = `环境 ${envId} 尚未开通云托管。请先调用 manageCloudRun(action="initEnv", envId="${envId}") 开通(异步、幂等),或前往控制台 环境 → 云托管 → 开通;Status=normal 后即可 deploy。`;
11258
+ }
11259
+ else if (status.status === "creating") {
11260
+ message = `环境 ${envId} 云托管正在开通中(Status=creating)。请稍后重试 manageCloudRun(action="deploy"),或用本 action 再次查询直到 Status=normal。`;
11261
+ }
11262
+ else if (status.status === "normal") {
11263
+ message = `环境 ${envId} 云托管已开通(Status=normal),可直接 manageCloudRun(action="deploy")。`;
11264
+ }
11265
+ else {
11266
+ message = `环境 ${envId} 云托管状态未知(Status=${status.status ?? "unknown"}),请稍后重试。`;
11267
+ }
11268
+ return {
11269
+ content: [
11270
+ {
11271
+ type: "text",
11272
+ text: JSON.stringify({
11273
+ success: true,
11274
+ data: {
11275
+ envId,
11276
+ status: status.status,
11277
+ isExist: status.isExist,
11278
+ ...(status.isExist
11279
+ ? {
11280
+ envBaseInfo: {
11281
+ Status: status.baseInfo.Status ?? null,
11282
+ PackageType: status.baseInfo.PackageType ?? null,
11283
+ Region: status.baseInfo.Region ?? null,
11284
+ EnvType: status.baseInfo.EnvType ?? null,
11285
+ },
11286
+ }
11287
+ : {}),
11288
+ },
11289
+ message
11290
+ }, null, 2)
11291
+ }
11292
+ ]
11293
+ };
11294
+ }
11295
+ default:
11296
+ throw new Error(`Unsupported action: ${input.action}`);
11297
+ }
11298
+ });
11299
+ // Track local running processes for CloudRun function services
11300
+ const runningProcesses = new Map();
11301
+ // Tool 2: Manage CloudRun services (write operations)
11302
+ server.registerTool("manageCloudRun", {
11303
+ title: "管理 CloudRun 服务",
11304
+ description: "管理云托管服务,按开发顺序支持:开通云托管环境(initEnv)、初始化项目(可从模板开始,模板列表可通过 queryCloudRun 查询)、下载服务代码、本地运行(仅函数型服务)、部署代码、仅更新配置(updateConfig,无需重新上传代码)、删除服务。deploy 支持两种方式:1) 源码构建(传入 targetPath,本地代码打包上传,默认路径);2) 已有镜像部署(传入 imageUrl,如 ccr.ccs.tencentyun.com/ns/img:v1,走 DeployType=image 容器型部署,targetPath 可省略)。deploy 语义为「触发部署 + 轻量等待任务注册」(最多约 45s)。源码构建返回 buildId,用 getDeployLog 轮询构建进度后再 getProcessLog;镜像部署(imageUrl)BuildId 常为 0,跳过 getDeployLog,返回 runId/next_step 引导 getProcessLog(或先 getDeployRecords 取 RunId)。若用户明确指定镜像或无需重新构建,必须传 imageUrl,不要仅因本地有源码目录就回退到源码构建。deploy 对已存在服务会先读取远程配置再合并(保留 VpcConf/EnvParams/OpenAccessTypes)。updateConfig 对齐控制台服务设置页。删除操作需要确认,建议设置force=true。新环境首次部署前若提示未开通云托管,先调用 initEnv 开通(异步、幂等)。",
11305
+ inputSchema: ManageCloudRunInputSchema,
11306
+ annotations: {
11307
+ readOnlyHint: false,
11308
+ destructiveHint: true,
11309
+ idempotentHint: false,
11310
+ openWorldHint: true,
11311
+ category: "cloudrun"
11312
+ }
11313
+ }, async (args) => {
11314
+ const input = args;
11315
+ const manager = await getManager();
11316
+ if (!manager) {
11317
+ throw new Error("Failed to initialize CloudBase manager. Please check your credentials and environment configuration.");
11318
+ }
11319
+ const cloudrunService = manager.cloudrun;
11320
+ let targetPath;
11321
+ if (input.targetPath) {
11322
+ targetPath = validateAndNormalizePath(input.targetPath);
11323
+ }
11324
+ switch (input.action) {
11325
+ case 'initEnv': {
11326
+ const envId = input.envId?.trim() || (await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions));
11327
+ const packageType = input.packageType || 'Trial';
11328
+ if (!manager.commonService) {
11329
+ throw new Error("Current CloudBase Manager does not support commonService; cannot initialize CloudRun env.");
11330
+ }
11331
+ // 幂等:先查当前开通状态,已开通 / 开通中不重复创建。
11332
+ let current;
11333
+ try {
11334
+ current = await describeCloudRunEnvStatus(manager, envId);
11335
+ }
11336
+ catch (error) {
11337
+ const baseMessage = error instanceof Error ? error.message : String(error);
11338
+ if (/ResourceNotFound|not.?initialized|未开通|未初始化/i.test(baseMessage) ||
11339
+ /InvalidParameter.*(?:Env|CloudRun)/i.test(baseMessage)) {
11340
+ current = { isExist: false, status: "unopened", baseInfo: {} };
11341
+ }
11342
+ else {
11343
+ throw new Error(`[manageCloudRun/initEnv] ${baseMessage}`);
11344
+ }
11345
+ }
11346
+ if (current.isExist && current.status === "normal") {
11347
+ return {
11348
+ content: [
11349
+ {
11350
+ type: "text",
11351
+ text: JSON.stringify({
11352
+ success: true,
11353
+ data: {
11354
+ envId,
11355
+ status: "normal",
11356
+ packageType: current.baseInfo.PackageType ?? packageType,
11357
+ created: false
11358
+ },
11359
+ message: `环境 ${envId} 已开通云托管(Status=normal),无需重复开通。可直接 manageCloudRun(action="deploy")。`
11360
+ }, null, 2)
11361
+ }
11362
+ ]
11363
+ };
11364
+ }
11365
+ if (current.isExist && current.status === "creating") {
11366
+ return {
11367
+ content: [
11368
+ {
11369
+ type: "text",
11370
+ text: JSON.stringify({
11371
+ success: true,
11372
+ data: {
11373
+ envId,
11374
+ status: "creating",
11375
+ created: false
11376
+ },
11377
+ message: `环境 ${envId} 云托管正在开通中(Status=creating),无需重复开通。请稍后用 queryCloudRun(action="envStatus", envId="${envId}") 查询,Status=normal 后即可 deploy。`
11378
+ }, null, 2)
11379
+ }
11380
+ ]
11381
+ };
11382
+ }
11383
+ // 未开通 → 发起异步开通(CreateCloudRunEnv 异步,不阻塞等待)。
11384
+ // Always pass EnvType=tcbr; optional vpcId/subnetIds when an explicit VPC is required.
11385
+ let createResult;
11386
+ try {
11387
+ createResult = await manager
11388
+ .commonService("tcbr", "2022-02-17")
11389
+ .call({
11390
+ Action: "CreateCloudRunEnv",
11391
+ Param: buildCreateCloudRunEnvParam({
11392
+ envId,
11393
+ packageType,
11394
+ vpcId: input.vpcId,
11395
+ subnetIds: input.subnetIds,
11396
+ }),
11397
+ });
11398
+ }
11399
+ catch (error) {
11400
+ throw new Error(buildManageCloudRunErrorMessage('initEnv', envId, error));
11401
+ }
11402
+ const tranId = createResult?.TranId ??
11403
+ createResult?.Response?.TranId ??
11404
+ createResult?.tranId;
11405
+ return {
11406
+ content: [
11407
+ {
11408
+ type: "text",
11409
+ text: JSON.stringify({
11410
+ success: true,
11411
+ data: {
11412
+ envId,
11413
+ status: "creating",
11414
+ packageType,
11415
+ created: true,
11416
+ ...(tranId ? { tranId } : {})
11417
+ },
11418
+ message: `已发起云托管开通(异步,Status=creating)。请稍后用 queryCloudRun(action="envStatus", envId="${envId}") 查询状态,Status=normal 后即可 manageCloudRun(action="deploy")。`
11419
+ }, null, 2)
11420
+ }
11421
+ ]
11422
+ };
11423
+ }
11424
+ case 'traffic': {
11425
+ const trafficOp = input.trafficOp;
11426
+ if (!trafficOp) {
11427
+ throw new Error("trafficOp is required for traffic operation (set | promote | rollback)");
11428
+ }
11429
+ if (trafficOp === 'set') {
11430
+ const stable = input.stablePercent;
11431
+ const canary = input.canaryPercent;
11432
+ if (typeof stable !== 'number' ||
11433
+ typeof canary !== 'number') {
11434
+ throw new Error("stablePercent and canaryPercent are required for trafficOp=set");
11435
+ }
11436
+ if (stable + canary !== 100) {
11437
+ throw new Error(`stablePercent + canaryPercent must equal 100 (got ${stable} + ${canary} = ${stable + canary}). ` +
11438
+ `Example: 90/10 means 90% to stable version, 10% to canary version.`);
11439
+ }
11440
+ let setResult;
11441
+ try {
11442
+ setResult = await cloudrunService.setTraffic(input.serverName, stable, canary);
11443
+ }
11444
+ catch (error) {
11445
+ throw new Error(buildManageCloudRunErrorMessage('traffic/set', input.serverName, error));
11446
+ }
11447
+ return {
11448
+ content: [
11449
+ {
11450
+ type: "text",
11451
+ text: JSON.stringify({
11452
+ success: true,
11453
+ data: {
11454
+ serverName: input.serverName,
11455
+ trafficOp: 'set',
11456
+ stablePercent: stable,
11457
+ canaryPercent: canary,
11458
+ result: setResult ?? null
11459
+ },
11460
+ message: `Set traffic for service '${input.serverName}': stable ${stable}% / canary ${canary}%`
11461
+ }, null, 2)
11462
+ }
11463
+ ]
11464
+ };
11465
+ }
11466
+ if (trafficOp === 'promote') {
11467
+ let promoteResult;
11468
+ try {
11469
+ promoteResult = await cloudrunService.promote(input.serverName);
11470
+ }
11471
+ catch (error) {
11472
+ throw new Error(buildManageCloudRunErrorMessage('traffic/promote', input.serverName, error));
11473
+ }
11474
+ return {
11475
+ content: [
11476
+ {
11477
+ type: "text",
11478
+ text: JSON.stringify({
11479
+ success: true,
11480
+ data: {
11481
+ serverName: input.serverName,
11482
+ trafficOp: 'promote',
11483
+ result: promoteResult ?? null
11484
+ },
11485
+ message: `Promoted canary version to full release for service '${input.serverName}' (100% traffic). This is irreversible.`
11486
+ }, null, 2)
11487
+ }
11488
+ ]
11489
+ };
11490
+ }
11491
+ // rollback
11492
+ let rollbackResult;
11493
+ try {
11494
+ rollbackResult = await cloudrunService.rollback(input.serverName);
11495
+ }
11496
+ catch (error) {
11497
+ throw new Error(buildManageCloudRunErrorMessage('traffic/rollback', input.serverName, error));
11498
+ }
10105
11499
  return {
10106
11500
  content: [
10107
11501
  {
@@ -10109,49 +11503,16 @@ function registerCloudRunTools(server) {
10109
11503
  text: JSON.stringify({
10110
11504
  success: true,
10111
11505
  data: {
10112
- buildId,
10113
- deployRecord: latestDeploy,
10114
- buildLog: buildLogResult?.Log || null,
10115
- processLogs,
10116
- combinedLogText,
10117
- ...(processLogsWarning ? { processLogsWarning } : {})
11506
+ serverName: input.serverName,
11507
+ trafficOp: 'rollback',
11508
+ result: rollbackResult ?? null
10118
11509
  },
10119
- message: `Retrieved deploy log for service '${serverName}'`
11510
+ message: `Rolled back service '${input.serverName}' to the previous stable version.`
10120
11511
  }, null, 2)
10121
11512
  }
10122
11513
  ]
10123
11514
  };
10124
11515
  }
10125
- default:
10126
- throw new Error(`Unsupported action: ${input.action}`);
10127
- }
10128
- });
10129
- // Track local running processes for CloudRun function services
10130
- const runningProcesses = new Map();
10131
- // Tool 2: Manage CloudRun services (write operations)
10132
- server.registerTool("manageCloudRun", {
10133
- title: "管理 CloudRun 服务",
10134
- description: "管理云托管服务,按开发顺序支持:初始化项目(可从模板开始,模板列表可通过 queryCloudRun 查询)、下载服务代码、本地运行(仅函数型服务)、部署代码、仅更新配置(updateConfig,无需重新上传代码)、删除服务。deploy 对已存在服务会先读取远程配置再合并(保留 VpcConf/EnvParams/OpenAccessTypes)。updateConfig 对齐控制台服务设置页。删除操作需要确认,建议设置force=true。",
10135
- inputSchema: ManageCloudRunInputSchema,
10136
- annotations: {
10137
- readOnlyHint: false,
10138
- destructiveHint: true,
10139
- idempotentHint: false,
10140
- openWorldHint: true,
10141
- category: "cloudrun"
10142
- }
10143
- }, async (args) => {
10144
- const input = args;
10145
- const manager = await getManager();
10146
- if (!manager) {
10147
- throw new Error("Failed to initialize CloudBase manager. Please check your credentials and environment configuration.");
10148
- }
10149
- const cloudrunService = manager.cloudrun;
10150
- let targetPath;
10151
- if (input.targetPath) {
10152
- targetPath = validateAndNormalizePath(input.targetPath);
10153
- }
10154
- switch (input.action) {
10155
11516
  case 'createAgent': {
10156
11517
  if (!targetPath) {
10157
11518
  throw new Error("targetPath is required for createAgent operation");
@@ -10317,14 +11678,18 @@ for await (let x of res.textStream) {
10317
11678
  };
10318
11679
  }
10319
11680
  case 'deploy': {
10320
- if (!targetPath) {
10321
- throw new Error("targetPath is required for deploy operation");
11681
+ if (!targetPath && !input.imageUrl) {
11682
+ throw new Error("targetPath (source build) or imageUrl (existing image) is required for deploy operation");
10322
11683
  }
10323
11684
  // Determine service type - use input.serverType if provided, otherwise auto-detect
10324
11685
  let serverType;
10325
11686
  let remoteServerConfig = null;
10326
11687
  let existingService = false;
10327
- if (input.serverType) {
11688
+ if (input.imageUrl) {
11689
+ // Image deploy is always container type (SDK: DeployInfo={DeployType:"image", ImageUrl}).
11690
+ serverType = 'container';
11691
+ }
11692
+ else if (input.serverType) {
10328
11693
  serverType = input.serverType;
10329
11694
  }
10330
11695
  else {
@@ -10378,6 +11743,17 @@ for await (let x of res.textStream) {
10378
11743
  // New service create path
10379
11744
  }
10380
11745
  }
11746
+ // 新环境必须先初始化云托管(大租户),否则 CreateCloudRunServer 会默认
11747
+ // 创建小租户服务。仅创建新服务(非 existing)前探测一次。
11748
+ // (2026-08-13 用户实测:luapi-v2 因未初始化云托管被创建到小租户)
11749
+ if (!existingService) {
11750
+ const currentEnvId = await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions);
11751
+ await ensureCloudRunEnvInitialized({
11752
+ cloudBaseOptions,
11753
+ envId: currentEnvId,
11754
+ serverName: input.serverName,
11755
+ });
11756
+ }
10381
11757
  let mergedFromRemote = [];
10382
11758
  let effectiveServerConfig = input.serverConfig;
10383
11759
  if (existingService) {
@@ -10395,18 +11771,37 @@ for await (let x of res.textStream) {
10395
11771
  force: input.force,
10396
11772
  serverType: serverType,
10397
11773
  };
11774
+ if (input.imageUrl) {
11775
+ deployParams.imageUrl = input.imageUrl;
11776
+ }
10398
11777
  if (effectiveServerConfig && Object.keys(effectiveServerConfig).length > 0) {
10399
11778
  deployParams.serverConfig = effectiveServerConfig;
10400
11779
  }
10401
11780
  // Manager SDK create path prefers top-level vpcInfo (CreateCloudRunServer.VpcInfo).
10402
- // Map serverConfig.VpcConf → vpcInfo so first-time create actually binds VPC.
11781
+ // Prefer serverConfig.VpcConf; if missing, auto-fill from env DescribeEnvBaseInfo
11782
+ // (envs opened with vpcId/subnetIds already carry VpcId/SubNetIds).
10403
11783
  const vpcConf = effectiveServerConfig?.VpcConf;
10404
- if (vpcConf?.VpcId?.trim() && vpcConf?.SubnetId?.trim()) {
10405
- deployParams.vpcInfo = {
10406
- VpcId: vpcConf.VpcId.trim(),
10407
- CreateType: 2,
10408
- SubnetIds: [vpcConf.SubnetId.trim()],
10409
- };
11784
+ let envBaseInfoForVpc = null;
11785
+ const explicitVpcId = vpcConf?.VpcId?.trim() ?? "";
11786
+ const explicitSubnetId = vpcConf?.SubnetId?.trim() ?? "";
11787
+ if (!explicitVpcId || !explicitSubnetId) {
11788
+ try {
11789
+ const envIdForVpc = await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions);
11790
+ const envStatusForVpc = await describeCloudRunEnvStatus(manager, envIdForVpc);
11791
+ if (envStatusForVpc.isExist) {
11792
+ envBaseInfoForVpc = envStatusForVpc.baseInfo;
11793
+ }
11794
+ }
11795
+ catch {
11796
+ // Best-effort: deploy may still succeed without vpcInfo when the platform creates the network.
11797
+ }
11798
+ }
11799
+ const resolvedVpcInfo = resolveCloudRunDeployVpcInfo({
11800
+ vpcConf,
11801
+ envBaseInfo: envBaseInfoForVpc,
11802
+ });
11803
+ if (resolvedVpcInfo) {
11804
+ deployParams.vpcInfo = resolvedVpcInfo;
10410
11805
  }
10411
11806
  let result;
10412
11807
  try {
@@ -10415,22 +11810,38 @@ for await (let x of res.textStream) {
10415
11810
  catch (error) {
10416
11811
  throw new Error(buildManageCloudRunErrorMessage('deploy', input.serverName, error));
10417
11812
  }
10418
- // Generate cloudbaserc.json configuration file
11813
+ // Generate cloudbaserc.json configuration file (source-build only; image deploy has no local project dir)
10419
11814
  const currentEnvId = await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions);
10420
- const cloudbasercPath = path_1.default.join(targetPath, 'cloudbaserc.json');
10421
- const cloudbasercContent = {
11815
+ // Lightweight wait for task / BuildId (source) or RunId (image) registration.
11816
+ // Does not wait for full build/deploy completion.
11817
+ const deployType = input.imageUrl
11818
+ ? "image"
11819
+ : "source";
11820
+ const registration = await waitForCloudRunDeployRegistration({
11821
+ manager,
11822
+ cloudrunService,
10422
11823
  envId: currentEnvId,
10423
- cloudrun: {
10424
- name: input.serverName
11824
+ serverName: input.serverName,
11825
+ mode: deployType,
11826
+ });
11827
+ let cloudbasercGenerated = false;
11828
+ if (targetPath) {
11829
+ const cloudbasercPath = path_1.default.join(targetPath, 'cloudbaserc.json');
11830
+ const cloudbasercContent = {
11831
+ envId: currentEnvId,
11832
+ cloudrun: {
11833
+ name: input.serverName
11834
+ }
11835
+ };
11836
+ try {
11837
+ fs_1.default.writeFileSync(cloudbasercPath, JSON.stringify(cloudbasercContent, null, 2));
11838
+ cloudbasercGenerated = true;
11839
+ }
11840
+ catch (error) {
11841
+ (0, logger_js_1.debug)('cloudbaserc.json creation skipped:', error instanceof Error ? error : new Error(String(error)));
10425
11842
  }
10426
- };
10427
- const consoleUrl = `https://tcb.cloud.tencent.com/dev?envId=${currentEnvId}#/platform-run/service/detail?serverName=${input.serverName}&tabId=overview&envId=${currentEnvId}`;
10428
- try {
10429
- fs_1.default.writeFileSync(cloudbasercPath, JSON.stringify(cloudbasercContent, null, 2));
10430
- }
10431
- catch (error) {
10432
- (0, logger_js_1.debug)('cloudbaserc.json creation skipped:', error instanceof Error ? error : new Error(String(error)));
10433
11843
  }
11844
+ const consoleUrl = `https://tcb.cloud.tencent.com/dev?envId=${currentEnvId}#/platform-run/service/detail?serverName=${input.serverName}&tabId=overview&envId=${currentEnvId}`;
10434
11845
  let preferredAccessUrl;
10435
11846
  let preferredAccessUrls = [];
10436
11847
  let preferredAccessSource;
@@ -10467,7 +11878,7 @@ for await (let x of res.textStream) {
10467
11878
  }
10468
11879
  // Send deployment notification to CodeBuddy IDE
10469
11880
  try {
10470
- const projectName = path_1.default.basename(targetPath);
11881
+ const projectName = targetPath ? path_1.default.basename(targetPath) : input.serverName;
10471
11882
  await (0, notification_js_1.sendDeployNotification)(server, {
10472
11883
  deployType: 'cloudrun',
10473
11884
  url: preferredAccessUrl ?? "",
@@ -10488,6 +11899,24 @@ for await (let x of res.textStream) {
10488
11899
  const warningSuffix = dbNetworkRisk
10489
11900
  ? ` Warning: ${dbNetworkRisk.message} Set serverConfig.VpcConf before relying on DB connectivity.`
10490
11901
  : "";
11902
+ const progressHint = buildCloudRunDeployProgressHint({
11903
+ deployType,
11904
+ serverName: input.serverName,
11905
+ buildId: registration.buildId,
11906
+ runId: registration.runId,
11907
+ registered: registration.registered,
11908
+ timedOut: registration.timedOut,
11909
+ waitMs: registration.waitMs,
11910
+ consoleUrl,
11911
+ taskId: registration.taskId,
11912
+ });
11913
+ const nextStep = buildCloudRunDeployNextStep({
11914
+ deployType,
11915
+ serverName: input.serverName,
11916
+ buildId: registration.buildId,
11917
+ runId: registration.runId,
11918
+ registered: registration.registered,
11919
+ });
10491
11920
  return {
10492
11921
  content: [
10493
11922
  {
@@ -10497,10 +11926,30 @@ for await (let x of res.textStream) {
10497
11926
  data: {
10498
11927
  serviceName: input.serverName,
10499
11928
  status: 'deploying',
10500
- deployPath: targetPath,
11929
+ deployType,
11930
+ ...(targetPath ? { deployPath: targetPath } : {}),
11931
+ ...(input.imageUrl ? { imageUrl: input.imageUrl } : {}),
10501
11932
  serverType: serverType,
10502
- cloudbasercGenerated: true,
11933
+ cloudbasercGenerated,
10503
11934
  consoleUrl,
11935
+ ...(isValidCloudRunBuildId(registration.buildId)
11936
+ ? { buildId: registration.buildId }
11937
+ : {}),
11938
+ ...(isValidCloudRunRunId(registration.runId)
11939
+ ? { runId: registration.runId }
11940
+ : {}),
11941
+ ...(typeof registration.taskId === "number"
11942
+ ? { taskId: registration.taskId }
11943
+ : {}),
11944
+ registration: {
11945
+ registered: registration.registered,
11946
+ timedOut: registration.timedOut,
11947
+ waitMs: registration.waitMs,
11948
+ ...(registration.taskStatus
11949
+ ? { taskStatus: registration.taskStatus }
11950
+ : {}),
11951
+ },
11952
+ next_step: nextStep,
10504
11953
  ...(existingService
10505
11954
  ? {
10506
11955
  configMerge: {
@@ -10521,8 +11970,10 @@ for await (let x of res.textStream) {
10521
11970
  ? { accessUrlSource: preferredAccessSource }
10522
11971
  : {}),
10523
11972
  ...(warnings.length > 0 ? { warnings } : {}),
11973
+ // Keep raw SDK result for debugging without implying build finished.
11974
+ ...(result != null ? { deployAccepted: true } : {}),
10524
11975
  },
10525
- message: `Triggered deployment for ${serverType} service '${input.serverName}' from ${targetPath}. You can follow the progress in ${consoleUrl}.${warningSuffix}`
11976
+ message: `Triggered deployment for ${serverType} service '${input.serverName}' ${input.imageUrl ? `from image ${input.imageUrl}` : `from ${targetPath}`}. Deployment is registering/${deployType === "image" ? "starting" : "building"} (status=deploying); this call does not wait for full completion.${progressHint}${warningSuffix}`
10526
11977
  }, null, 2)
10527
11978
  }
10528
11979
  ]
@@ -11578,6 +13029,13 @@ function registerAppAuthTools(server) {
11578
13029
  }
11579
13030
 
11580
13031
 
13032
+ /***/ }),
13033
+
13034
+ /***/ 5663:
13035
+ /***/ ((module) => {
13036
+
13037
+ module.exports = __WEBPACK_EXTERNAL_MODULE_node_path_02319fef__;
13038
+
11581
13039
  /***/ }),
11582
13040
 
11583
13041
  /***/ 5814:
@@ -11650,7 +13108,7 @@ class TelemetryReporter {
11650
13108
  const arch = os_1.default.arch(); // 系统架构
11651
13109
  // Build-time injected version; guard for vitest / non-webpack runtimes.
11652
13110
  const mcpVersion = process.env.npm_package_version ||
11653
- ( true ? "2.26.0" : 0);
13111
+ ( true ? "2.28.0" : 0);
11654
13112
  return {
11655
13113
  userAgent: `${osType} ${osRelease} ${arch} ${nodeVersion} CloudBase-MCP/${mcpVersion}`,
11656
13114
  deviceId: this.deviceId,
@@ -11984,8 +13442,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11984
13442
  return (mod && mod.__esModule) ? mod : { "default": mod };
11985
13443
  };
11986
13444
  Object.defineProperty(exports, "__esModule", ({ value: true }));
11987
- exports.CUSTOM_IMAGE_RUNTIME = exports.MANAGE_FUNCTION_ACTIONS = exports.QUERY_FUNCTION_ACTIONS = exports.TRIGGER_CONFIG_EXAMPLES = exports.SUPPORTED_TRIGGER_TYPES = exports.DEFAULT_NODEJS_RUNTIME = exports.SUPPORTED_NODEJS_RUNTIMES = exports.RECOMMENDED_RUNTIMES = exports.DEFAULT_RUNTIME = exports.ALL_SUPPORTED_RUNTIMES = exports.SUPPORTED_RUNTIMES = void 0;
13445
+ exports.CUSTOM_IMAGE_RUNTIME = exports.LAYER_SOFT_WARN = exports.MANAGE_FUNCTION_ACTIONS = exports.QUERY_FUNCTION_ACTIONS = exports.TRIGGER_CONFIG_EXAMPLES = exports.SUPPORTED_TRIGGER_TYPES = exports.DEFAULT_NODEJS_RUNTIME = exports.SUPPORTED_NODEJS_RUNTIMES = exports.RECOMMENDED_RUNTIMES = exports.DEFAULT_RUNTIME = exports.ALL_SUPPORTED_RUNTIMES = exports.SUPPORTED_RUNTIMES = void 0;
11988
13446
  exports.formatRuntimeList = formatRuntimeList;
13447
+ exports.layerNameIncludesEnvId = layerNameIncludesEnvId;
13448
+ exports.buildCreateLayerNameWarning = buildCreateLayerNameWarning;
11989
13449
  exports.validateTimerCron = validateTimerCron;
11990
13450
  exports.pickManageFunctionName = pickManageFunctionName;
11991
13451
  exports.shouldInstallDependencyForFunction = shouldInstallDependencyForFunction;
@@ -12090,6 +13550,24 @@ exports.MANAGE_FUNCTION_ACTIONS = [
12090
13550
  "updateFunctionLayers",
12091
13551
  "incrementalDeployFunction", // 增量部署,需通过 pluginOptions.functions 注入实现
12092
13552
  ];
13553
+ /** Layer soft-warn copy — account-scoped SCF LayerName guidance (no hard fail). */
13554
+ exports.LAYER_SOFT_WARN = {
13555
+ createNameFormat: (envId) => `建议使用 {layerName}_${envId} 格式,当前名称可能与其他环境共享版本序列`,
13556
+ deleteVersion: "该层为账号级共享资源,删除版本会影响所有绑定该版本的环境的函数,请确认",
13557
+ bindShared: "层为账号级共享,绑定/解绑影响所有引用该层名的环境",
13558
+ accountLevelView: "返回账号级视图,含其他环境创建的层",
13559
+ };
13560
+ function layerNameIncludesEnvId(layerName, envId) {
13561
+ if (!envId)
13562
+ return true;
13563
+ return layerName.includes(envId);
13564
+ }
13565
+ function buildCreateLayerNameWarning(layerName, envId) {
13566
+ if (!envId || layerNameIncludesEnvId(layerName, envId)) {
13567
+ return undefined;
13568
+ }
13569
+ return exports.LAYER_SOFT_WARN.createNameFormat(envId);
13570
+ }
12093
13571
  const VPC_SCHEMA = zod_1.z.object({
12094
13572
  vpcId: zod_1.z
12095
13573
  .string()
@@ -12331,11 +13809,12 @@ function registerFunctionTools(server) {
12331
13809
  const cloudBaseOptions = server.cloudBaseOptions;
12332
13810
  const deployOverrides = server.pluginOptions?.functions;
12333
13811
  const getManager = () => (0, cloudbase_manager_js_1.getCloudBaseManager)({ cloudBaseOptions });
12334
- const buildEnvelope = (data, message, nextActions) => ({
13812
+ const buildEnvelope = (data, message, nextActions, warnings) => ({
12335
13813
  success: true,
12336
13814
  data,
12337
13815
  message,
12338
13816
  ...(nextActions?.length ? { nextActions } : {}),
13817
+ ...(warnings?.length ? { warnings } : {}),
12339
13818
  });
12340
13819
  const buildErrorEnvelope = (error, errorCode) => ({
12341
13820
  success: false,
@@ -12586,7 +14065,7 @@ function registerFunctionTools(server) {
12586
14065
  action: "createLayerVersion",
12587
14066
  reason: "发布新的层版本",
12588
14067
  },
12589
- ]);
14068
+ ], [exports.LAYER_SOFT_WARN.accountLevelView]);
12590
14069
  }
12591
14070
  case "listLayerVersions": {
12592
14071
  if (!input.layerName) {
@@ -12614,7 +14093,7 @@ function registerFunctionTools(server) {
12614
14093
  action: "attachLayer",
12615
14094
  reason: "将某个层版本绑定到函数",
12616
14095
  },
12617
- ]);
14096
+ ], [exports.LAYER_SOFT_WARN.accountLevelView]);
12618
14097
  }
12619
14098
  case "getLayerVersionDetail": {
12620
14099
  if (!input.layerName) {
@@ -12647,7 +14126,7 @@ function registerFunctionTools(server) {
12647
14126
  action: "deleteLayerVersion",
12648
14127
  reason: "删除该层版本",
12649
14128
  },
12650
- ]);
14129
+ ], [exports.LAYER_SOFT_WARN.accountLevelView]);
12651
14130
  }
12652
14131
  case "listFunctionTriggers": {
12653
14132
  if (!input.functionName) {
@@ -13163,6 +14642,8 @@ function registerFunctionTools(server) {
13163
14642
  if (!input.contentPath && !input.base64Content) {
13164
14643
  throw new Error("createLayerVersion 操作时,contentPath 和 base64Content 至少需要提供一个");
13165
14644
  }
14645
+ const envId = await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions);
14646
+ const nameWarning = buildCreateLayerNameWarning(input.layerName, envId);
13166
14647
  const cloudbase = await getManager();
13167
14648
  const result = await cloudbase.functions.createLayer({
13168
14649
  name: input.layerName,
@@ -13185,7 +14666,7 @@ function registerFunctionTools(server) {
13185
14666
  action: "listLayerVersions",
13186
14667
  reason: "查看该层的全部版本",
13187
14668
  },
13188
- ]);
14669
+ ], nameWarning ? [nameWarning] : undefined);
13189
14670
  }
13190
14671
  case "deleteLayerVersion": {
13191
14672
  if (!input.layerName) {
@@ -13212,7 +14693,7 @@ function registerFunctionTools(server) {
13212
14693
  action: "listLayerVersions",
13213
14694
  reason: "确认剩余层版本",
13214
14695
  },
13215
- ]);
14696
+ ], [exports.LAYER_SOFT_WARN.deleteVersion]);
13216
14697
  }
13217
14698
  case "attachLayer":
13218
14699
  case "detachLayer":
@@ -13222,6 +14703,7 @@ function registerFunctionTools(server) {
13222
14703
  }
13223
14704
  const cloudbase = await getManager();
13224
14705
  const envId = await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions);
14706
+ const bindWarnings = [exports.LAYER_SOFT_WARN.bindShared];
13225
14707
  if (input.action === "attachLayer") {
13226
14708
  if (!input.layerName) {
13227
14709
  throw new Error("attachLayer 操作时,layerName 参数是必需的");
@@ -13250,7 +14732,7 @@ function registerFunctionTools(server) {
13250
14732
  action: "listFunctionLayers",
13251
14733
  reason: "确认函数当前绑定层列表",
13252
14734
  },
13253
- ]);
14735
+ ], bindWarnings);
13254
14736
  }
13255
14737
  if (input.action === "detachLayer") {
13256
14738
  if (!input.layerName) {
@@ -13281,7 +14763,7 @@ function registerFunctionTools(server) {
13281
14763
  action: "listFunctionLayers",
13282
14764
  reason: "确认解绑后的层列表",
13283
14765
  },
13284
- ]);
14766
+ ], bindWarnings);
13285
14767
  }
13286
14768
  const normalizedLayers = normalizeManageLayers(input.layers);
13287
14769
  if (!normalizedLayers.length) {
@@ -13306,7 +14788,7 @@ function registerFunctionTools(server) {
13306
14788
  action: "listFunctionLayers",
13307
14789
  reason: "确认最新层顺序和绑定结果",
13308
14790
  },
13309
- ]);
14791
+ ], bindWarnings);
13310
14792
  }
13311
14793
  default:
13312
14794
  // incrementalDeployFunction:无默认实现,必须通过 pluginOptions 注入
@@ -13336,6 +14818,10 @@ function registerFunctionTools(server) {
13336
14818
  "\n- 示例:`queryFunctions(action=\"listFunctionLogs\", functionName=\"my-function\")`" +
13337
14819
  "\n- 如需查看日志详情:`queryFunctions(action=\"getFunctionLogDetail\", requestId=\"xxx\")`" +
13338
14820
  "\n\n**定时任务 / cron / 定时跑**:使用 `listFunctionTriggers` 查询函数的 timer 触发器配置。" +
14821
+ "\n\n**层(Layer)说明**:" +
14822
+ "\n- 层为 SCF 账号级共享命名空间:不同环境创建同名层会共享同一层的版本序列;删除某版本会影响所有绑定该版本的环境的函数" +
14823
+ "\n- 创建层必须用带环境标识的唯一层名,固定格式:`{layerName}_{当前envId}`(如 `common_cloud1-d9ghadgak3edf6b36`)。不要在不同环境使用相同裸层名,创建前先 `listLayers` 查重" +
14824
+ "\n- `listLayers` / `listLayerVersions` / `getLayerVersionDetail` 返回账号级视图,可能含其他环境创建的层" +
13339
14825
  "\n\n**区分 `queryLogs` 工具**:" +
13340
14826
  "\n- 本工具用于查询特定 CloudBase 云函数的执行日志" +
13341
14827
  "\n- `queryLogs` 工具用于搜索 CLS 日志服务(跨服务日志聚合)",
@@ -13348,9 +14834,9 @@ function registerFunctionTools(server) {
13348
14834
  "\n- `listFunctionLogs`: 查询 CloudBase 云函数执行日志(需要 functionName)" +
13349
14835
  "\n- `getFunctionLogDetail`: 获取日志详情(需要 requestId)" +
13350
14836
  "\n- `listFunctionLayers`: 列出函数绑定的层" +
13351
- "\n- `listLayers`: 列出所有层" +
13352
- "\n- `listLayerVersions`: 列出层的版本(注意:是 Versions 不是 Version)" +
13353
- "\n- `getLayerVersionDetail`: 获取层版本详情" +
14837
+ "\n- `listLayers`: 列出所有层(账号级视图,含其他环境创建的层)" +
14838
+ "\n- `listLayerVersions`: 列出层的版本(注意:是 Versions 不是 Version;账号级视图)" +
14839
+ "\n- `getLayerVersionDetail`: 获取层版本详情(账号级视图)" +
13354
14840
  "\n- `listFunctionTriggers`: 列出函数触发器(用于查看定时任务 / cron / timer 配置)" +
13355
14841
  "\n- `getFunctionDownloadUrl`: 获取函数代码下载地址"),
13356
14842
  functionName: zod_1.z
@@ -13377,7 +14863,11 @@ function registerFunctionTools(server) {
13377
14863
  qualifier: zod_1.z.string().optional().describe("函数版本别名,如 $LATEST、$DEFAULT。日志查询时可选"),
13378
14864
  runtime: zod_1.z.string().optional().describe("层查询的运行时筛选,如 Nodejs18.15"),
13379
14865
  searchKey: zod_1.z.string().optional().describe("层名称搜索关键字"),
13380
- layerName: zod_1.z.string().optional().describe("层名称。`listLayerVersions`、`getLayerVersionDetail` 操作必填"),
14866
+ layerName: zod_1.z
14867
+ .string()
14868
+ .optional()
14869
+ .describe("层名称。`listLayerVersions`、`getLayerVersionDetail` 操作必填。" +
14870
+ "层为账号级共享命名空间;推荐固定格式 `{layerName}_{当前envId}`(如 common_cloud1-d9ghadgak3edf6b36)"),
13381
14871
  layerVersion: zod_1.z.number().optional().describe("层版本号。`getLayerVersionDetail` 操作必填"),
13382
14872
  },
13383
14873
  annotations: {
@@ -13392,12 +14882,18 @@ function registerFunctionTools(server) {
13392
14882
  "如果要创建 cron 定时任务,先用 createFunction 创建函数,再用 createFunctionTrigger 创建 timer 触发器(支持7段cron表达式),deleteFunctionTrigger 删除触发器。" +
13393
14883
  "镜像部署(Runtime=CustomImage):先把代码经 zip→COS→CloudApp custom 构建→TCR 推镜像(这一阶段为裸腾讯云 API,本工具不覆盖)," +
13394
14884
  "再用 createFunction(func.runtime=\"CustomImage\", imageConfig) 基于 TCR 镜像创建 HTTP 函数;后续迭代用 updateFunctionCode + imageConfig 换镜像 tag。" +
13395
- "危险操作需要显式 confirm=true。",
14885
+ "危险操作需要显式 confirm=true。" +
14886
+ "\n\n**层(Layer)说明**:" +
14887
+ "\n- 层为 SCF 账号级共享命名空间:不同环境创建同名层会共享同一层的版本序列;删除某版本会影响所有绑定该版本的环境的函数" +
14888
+ "\n- 创建层必须用带环境标识的唯一层名,固定格式:`{layerName}_{当前envId}`(如 `common_cloud1-d9ghadgak3edf6b36`)。不要在不同环境使用相同裸层名,创建前先 `listLayers` 查重" +
14889
+ "\n- 相关 action:`createLayerVersion` / `deleteLayerVersion` / `attachLayer` / `detachLayer` / `updateFunctionLayers`(只读查询见 queryFunctions 的 listLayers / listLayerVersions / getLayerVersionDetail)",
13396
14890
  inputSchema: {
13397
14891
  action: zod_1.z
13398
14892
  .enum(exports.MANAGE_FUNCTION_ACTIONS)
13399
14893
  .describe("写操作类型,例如 createFunction、updateFunctionCode、incrementalDeployFunction、invokeFunction、deleteFunction、" +
13400
- "createFunctionTrigger(定时任务 / cron / timer)、deleteFunctionTrigger、attachLayer、detachLayer"),
14894
+ "createFunctionTrigger(定时任务 / cron / timer)、deleteFunctionTrigger、" +
14895
+ "createLayerVersion、deleteLayerVersion、attachLayer、detachLayer、updateFunctionLayers。" +
14896
+ "层名推荐固定格式 `{layerName}_{当前envId}`(如 common_cloud1-d9ghadgak3edf6b36)"),
13401
14897
  func: CREATE_FUNCTION_SCHEMA.optional().describe("createFunction 操作的函数配置"),
13402
14898
  functionRootPath: zod_1.z.string().optional().describe("创建或更新函数代码时默认推荐的本地目录方式。" +
13403
14899
  "必须是直接包含函数文件夹的目录绝对路径(如 /abs/path/cloudfunctions 或 /abs/path/functions)," +
@@ -13427,7 +14923,11 @@ function registerFunctionTools(server) {
13427
14923
  .describe("createFunctionTrigger 的触发器列表,用于定时跑 / 定时任务 / scheduled job。timer 触发器使用7段 cron 表达式(秒 分 时 日 月 星期 年)," +
13428
14924
  '如 "0 */5 * * * * *" 表示每5分钟执行一次'),
13429
14925
  triggerName: zod_1.z.string().optional().describe("deleteFunctionTrigger 的目标触发器名称"),
13430
- layerName: zod_1.z.string().optional().describe("层名称"),
14926
+ layerName: zod_1.z
14927
+ .string()
14928
+ .optional()
14929
+ .describe("层名称。创建层推荐固定格式 `{layerName}_{当前envId}`(如 common_cloud1-d9ghadgak3edf6b36);" +
14930
+ "不要跨环境复用裸层名。层为账号级共享命名空间"),
13431
14931
  layerVersion: zod_1.z.number().optional().describe("层版本号"),
13432
14932
  contentPath: zod_1.z.string().optional().describe("层内容路径,可为目录或 ZIP 文件"),
13433
14933
  base64Content: zod_1.z.string().optional().describe("层内容的 base64 编码"),
@@ -13559,6 +15059,98 @@ async function resolvePgDbContext(cloudBaseOptions, args) {
13559
15059
  role: args?.role ?? "cloudbase_admin",
13560
15060
  };
13561
15061
  }
15062
+ /** Default / recommended roles for CloudBase PG ExecutePGSql.Role */
15063
+ const PG_DEFAULT_ROLE = "cloudbase_admin";
15064
+ const PG_RECOMMENDED_ROLES = [
15065
+ "cloudbase_admin",
15066
+ "anon",
15067
+ "authenticated",
15068
+ "service_role",
15069
+ ];
15070
+ /**
15071
+ * Roles that look like login/instance principals and routinely fail SET ROLE
15072
+ * in beacon data (e.g. postgres, postgres_pgdb_<id>). Reject before the API call.
15073
+ */
15074
+ function isLikelyInvalidPgExecuteRole(role) {
15075
+ const normalized = role.trim();
15076
+ if (!normalized) {
15077
+ return true;
15078
+ }
15079
+ if (normalized === "postgres") {
15080
+ return true;
15081
+ }
15082
+ // Instance-derived login principals, not valid ExecutePGSql Role targets
15083
+ if (/^postgres_pgdb_[a-z0-9]+$/i.test(normalized)) {
15084
+ return true;
15085
+ }
15086
+ // AI-invented concatenations such as cloudbase_admin_pgdb_xxx_pgdb_xxx
15087
+ if (/_pgdb_[a-z0-9]+(_pgdb_[a-z0-9]+)?$/i.test(normalized) &&
15088
+ !PG_RECOMMENDED_ROLES.includes(normalized)) {
15089
+ return true;
15090
+ }
15091
+ return false;
15092
+ }
15093
+ function extractPgRoleNameFromError(message) {
15094
+ const missing = message.match(/role\s+"([^"]+)"\s+does\s+not\s+exist/i);
15095
+ if (missing?.[1]) {
15096
+ return missing[1];
15097
+ }
15098
+ const setRole = message.match(/set\s+role\s+([^\s:]+)/i);
15099
+ if (setRole?.[1]) {
15100
+ return setRole[1].replace(/^"|"$/g, "");
15101
+ }
15102
+ const denied = message.match(/permission\s+denied\s+to\s+set\s+role\s+"([^"]+)"/i);
15103
+ if (denied?.[1]) {
15104
+ return denied[1];
15105
+ }
15106
+ return null;
15107
+ }
15108
+ function isPgRoleExecutionError(message) {
15109
+ const lower = message.toLowerCase();
15110
+ return ((lower.includes("set role") &&
15111
+ (lower.includes("does not exist") ||
15112
+ lower.includes("permission denied"))) ||
15113
+ /role\s+"[^"]+"\s+does\s+not\s+exist/i.test(message) ||
15114
+ /permission\s+denied\s+to\s+set\s+role/i.test(message));
15115
+ }
15116
+ function buildPgRoleGuidanceMessage(attemptedRole) {
15117
+ const recommended = PG_RECOMMENDED_ROLES.join(", ");
15118
+ return (`PostgreSQL role "${attemptedRole}" cannot be used with ExecutePGSql (SET ROLE failed or role does not exist). ` +
15119
+ `Do not invent roles like postgres / postgres_pgdb_* from env or instance names. ` +
15120
+ `Omit role (defaults to ${PG_DEFAULT_ROLE}) or pass one of: ${recommended}. ` +
15121
+ `To list roles that exist in this database, retry with role=${PG_DEFAULT_ROLE} and SQL: SELECT rolname FROM pg_roles ORDER BY rolname;`);
15122
+ }
15123
+ function buildPgRoleErrorPayload(attemptedRole, rawMessage) {
15124
+ return {
15125
+ success: false,
15126
+ errorCode: "PG_ROLE_NOT_AVAILABLE",
15127
+ message: buildPgRoleGuidanceMessage(attemptedRole),
15128
+ data: {
15129
+ attemptedRole,
15130
+ defaultRole: PG_DEFAULT_ROLE,
15131
+ recommendedRoles: [...PG_RECOMMENDED_ROLES],
15132
+ listRolesSql: "SELECT rolname FROM pg_roles ORDER BY rolname;",
15133
+ ...(rawMessage ? { rawError: rawMessage.slice(0, 500) } : {}),
15134
+ },
15135
+ nextActions: [
15136
+ buildNextAction(MANAGE_PG_DATABASE, "execute", `Retry without a custom role (defaults to ${PG_DEFAULT_ROLE}), or pass role=${PG_DEFAULT_ROLE} explicitly.`, {
15137
+ action: "execute",
15138
+ role: PG_DEFAULT_ROLE,
15139
+ sql: "SELECT rolname FROM pg_roles ORDER BY rolname;",
15140
+ confirm: true,
15141
+ }),
15142
+ buildNextAction(QUERY_PG_DATABASE, "context", "Inspect the auto-derived PG context (includes default role) before retrying.", { action: "context" }),
15143
+ ],
15144
+ };
15145
+ }
15146
+ function tryBuildPgRoleErrorPayload(error, fallbackRole) {
15147
+ const rawMessage = error instanceof Error ? error.message : String(error);
15148
+ if (!isPgRoleExecutionError(rawMessage)) {
15149
+ return null;
15150
+ }
15151
+ const attemptedRole = extractPgRoleNameFromError(rawMessage) ?? fallbackRole;
15152
+ return buildPgRoleErrorPayload(attemptedRole, rawMessage);
15153
+ }
13562
15154
  function normalizeLimit(limit, fallback = 20, max = 200) {
13563
15155
  if (!Number.isFinite(limit)) {
13564
15156
  return fallback;
@@ -14783,7 +16375,26 @@ async function handleReadOnlySql(args, context, deps) {
14783
16375
  }
14784
16376
  const limit = normalizeLimit(args.limit);
14785
16377
  const limitedSql = buildLimitedReadOnlySql(args.sql, limit);
14786
- const result = await withPgClient(context, deps, (client) => client.query(limitedSql));
16378
+ let result;
16379
+ try {
16380
+ result = await withPgClient(context, deps, (client) => client.query(limitedSql));
16381
+ }
16382
+ catch (error) {
16383
+ const rolePayload = tryBuildPgRoleErrorPayload(error, context.role);
16384
+ if (rolePayload) {
16385
+ return buildPgToolResult(rolePayload);
16386
+ }
16387
+ const reason = error instanceof Error ? error.message : String(error);
16388
+ return buildPgToolResult({
16389
+ success: false,
16390
+ errorCode: "PG_SQL_EXEC_FAILED",
16391
+ message: `PostgreSQL read-only SQL execution failed: ${reason}`,
16392
+ data: {
16393
+ role: context.role,
16394
+ sqlPreview: limitedSql.trim().slice(0, 500),
16395
+ },
16396
+ });
16397
+ }
14787
16398
  const summary = summarizeQueryResult(result, limit);
14788
16399
  return buildPgToolResult({
14789
16400
  success: true,
@@ -14838,6 +16449,9 @@ async function handleExecuteSql(args, context, deps) {
14838
16449
  ],
14839
16450
  });
14840
16451
  }
16452
+ if (args.role !== undefined && isLikelyInvalidPgExecuteRole(args.role)) {
16453
+ return buildPgToolResult(buildPgRoleErrorPayload(args.role.trim() || "(empty)"));
16454
+ }
14841
16455
  if (!classification.readOnly && !args.confirm) {
14842
16456
  return buildPgToolResult({
14843
16457
  success: false,
@@ -14857,7 +16471,26 @@ async function handleExecuteSql(args, context, deps) {
14857
16471
  ],
14858
16472
  });
14859
16473
  }
14860
- const result = await withPgClient(context, deps, (client) => client.query(args.sql));
16474
+ let result;
16475
+ try {
16476
+ result = await withPgClient(context, deps, (client) => client.query(args.sql));
16477
+ }
16478
+ catch (error) {
16479
+ const rolePayload = tryBuildPgRoleErrorPayload(error, context.role);
16480
+ if (rolePayload) {
16481
+ return buildPgToolResult(rolePayload);
16482
+ }
16483
+ const reason = error instanceof Error ? error.message : String(error);
16484
+ return buildPgToolResult({
16485
+ success: false,
16486
+ errorCode: "PG_SQL_EXEC_FAILED",
16487
+ message: `PostgreSQL SQL execution failed: ${reason}`,
16488
+ data: {
16489
+ role: context.role,
16490
+ sqlPreview: args.sql.trim().slice(0, 500),
16491
+ },
16492
+ });
16493
+ }
14861
16494
  const targetTable = parseTargetTableFromSql(args.sql, context.defaultSchema);
14862
16495
  return buildPgToolResult({
14863
16496
  success: true,
@@ -15750,7 +17383,9 @@ function registerPGDatabaseTools(server, providedDeps) {
15750
17383
  role: zod_1.z
15751
17384
  .string()
15752
17385
  .optional()
15753
- .describe("可选的 PostgreSQL role,会传给 Manager SDK executePGSql;例如需要管理策略时可传 postgres。"),
17386
+ .describe(`可选的 PostgreSQL role,传给 Manager SDK executePGSql 的 Role(平台会 SET ROLE)。默认 ${PG_DEFAULT_ROLE}。` +
17387
+ `推荐取值:${PG_RECOMMENDED_ROLES.join(" / ")}。` +
17388
+ `不要传 postgres、postgres_pgdb_* 或从环境名臆造的角色;不确定时省略本字段,或先用 ${PG_DEFAULT_ROLE} 执行 SELECT rolname FROM pg_roles。`),
15754
17389
  objectName: zod_1.z
15755
17390
  .string()
15756
17391
  .optional()
@@ -15854,6 +17489,10 @@ function registerPGDatabaseTools(server, providedDeps) {
15854
17489
  return handleExecuteSql(args, context, deps);
15855
17490
  }
15856
17491
  }
17492
+ // Reject known-bad Role values before readiness probe / ExecutePGSql SET ROLE.
17493
+ if (args.role !== undefined && isLikelyInvalidPgExecuteRole(args.role)) {
17494
+ return buildPgToolResult(buildPgRoleErrorPayload(args.role.trim() || "(empty)"));
17495
+ }
15857
17496
  try {
15858
17497
  await ensurePgReadyOnce(cbOpts, deps);
15859
17498
  }
@@ -15910,6 +17549,7 @@ function registerPGDatabaseTools(server, providedDeps) {
15910
17549
 
15911
17550
 
15912
17551
  Object.defineProperty(exports, "__esModule", ({ value: true }));
17552
+ exports.validateUserRegoContent = validateUserRegoContent;
15913
17553
  exports.registerPermissionTools = registerPermissionTools;
15914
17554
  const zod_1 = __webpack_require__(2971);
15915
17555
  const cloudbase_manager_js_1 = __webpack_require__(3431);
@@ -15921,6 +17561,9 @@ const QUERY_PERMISSION_ACTIONS = [
15921
17561
  "getRole",
15922
17562
  "listUsers",
15923
17563
  "getUser",
17564
+ // Align with CLI `tcb policy list/get` (OPA gateway authz)
17565
+ "listPolicy",
17566
+ "getPolicy",
15924
17567
  ];
15925
17568
  const MANAGE_PERMISSION_ACTIONS = [
15926
17569
  "updateResourcePermission",
@@ -15934,7 +17577,14 @@ const MANAGE_PERMISSION_ACTIONS = [
15934
17577
  "createUser",
15935
17578
  "updateUser",
15936
17579
  "deleteUsers",
17580
+ // Align with CLI `tcb policy set` (OPA Rego; disables legacy gateway auth)
17581
+ "setPolicy",
15937
17582
  ];
17583
+ /** Keys accepted by Manager SDK describeEnvAuthzConfig / modifyEnvAuthzConfig. */
17584
+ const AUTHZ_USER_REGO_KEY = "authz.user.rego";
17585
+ const AUTHZ_PLATFORM_EXTENSION_REGO_KEY = "authz.platform.extension.rego";
17586
+ /** CLI `tcb policy list --resource-type` only documents `policy`. */
17587
+ const POLICY_LIST_RESOURCE_TYPES = ["policy"];
15938
17588
  function buildWriteVerificationHint(resourceId) {
15939
17589
  return `对于 ${resourceId} 这类有后端权限控制的集合,前端调用 .doc(id).update() / .doc(id).remove() 后,不能只看是否没有抛异常。请显式检查返回结果中的 updated / deleted 是否大于 0;如果 result.code、result.message 存在,或 updated / deleted 为 0,要把它当作真实失败并向上抛错。`;
15940
17590
  }
@@ -15973,10 +17623,32 @@ function isPostgresqlPermissionApiUnsupported(error) {
15973
17623
  const message = error instanceof Error ? error.message : String(error);
15974
17624
  return /does not support PostgreSQL type environments/i.test(message);
15975
17625
  }
15976
- const AUTHZ_USER_REGO_KEY = "authz.user.rego";
15977
17626
  function looksLikeUserRego(value) {
15978
17627
  return /^\s*package\s+authz\.user\b/m.test(value);
15979
17628
  }
17629
+ /**
17630
+ * Validate user Rego before setPolicy / modifyEnvAuthzConfig.
17631
+ * Aligns with CLI docs (must start with `package authz.user`) and CLI non-empty check.
17632
+ * Does not run a full OPA compiler; backend still owns deep syntax validation.
17633
+ */
17634
+ function validateUserRegoContent(value) {
17635
+ if (typeof value !== "string" || value.trim().length === 0) {
17636
+ throw new Error("action=setPolicy 需要非空 regoContent(对齐 CLI `tcb policy set <regoContent>`)。");
17637
+ }
17638
+ const trimmed = value.trim();
17639
+ if (!looksLikeUserRego(trimmed)) {
17640
+ throw new Error("Rego 策略必须以 `package authz.user` 开头(对齐 https://docs.cloudbase.net/cli-v1/policy/management )。");
17641
+ }
17642
+ const open = (trimmed.match(/\{/g) || []).length;
17643
+ const close = (trimmed.match(/\}/g) || []).length;
17644
+ if (open !== close) {
17645
+ throw new Error(`Rego 策略花括号不匹配({=${open}, }=${close})。请检查语法后再调用 setPolicy。`);
17646
+ }
17647
+ return trimmed;
17648
+ }
17649
+ function resolveAuthzConfigKey(extension) {
17650
+ return extension ? AUTHZ_PLATFORM_EXTENSION_REGO_KEY : AUTHZ_USER_REGO_KEY;
17651
+ }
15980
17652
  /** Detect legacy function securityRule shapes that mean "public invoke". */
15981
17653
  function isPublicFunctionInvokeRule(securityRule) {
15982
17654
  if (!securityRule || securityRule.trim() === "") {
@@ -16048,29 +17720,38 @@ function resolveFunctionAuthzRegoInput(securityRule, resourceId) {
16048
17720
  `2) permission="CUSTOM" with securityRule='{"invoke":true}' to generate a public-functions allow policy.\n` +
16049
17721
  `See https://docs.cloudbase.net/cli-v1/policy/management`);
16050
17722
  }
16051
- async function describeEnvAuthzUserRego(cloudbase) {
17723
+ async function describeEnvAuthzConfigByKey(cloudbase, key = AUTHZ_USER_REGO_KEY) {
16052
17724
  if (!cloudbase?.permission?.describeEnvAuthzConfig) {
16053
17725
  throw new Error("Current @cloudbase/manager-node does not expose permission.describeEnvAuthzConfig. Upgrade manager-node (>= 5.5.5) to align with CLI tcb policy get.");
16054
17726
  }
16055
- const result = await cloudbase.permission.describeEnvAuthzConfig({
16056
- key: AUTHZ_USER_REGO_KEY,
16057
- });
17727
+ const result = await cloudbase.permission.describeEnvAuthzConfig({ key });
16058
17728
  const value = typeof result?.Item?.Value === "string"
16059
17729
  ? result.Item.Value
16060
17730
  : typeof result?.Value === "string"
16061
17731
  ? result.Value
16062
17732
  : "";
16063
- return { value, raw: result };
17733
+ return { key, value, raw: result };
17734
+ }
17735
+ async function describeEnvAuthzUserRego(cloudbase) {
17736
+ const result = await describeEnvAuthzConfigByKey(cloudbase, AUTHZ_USER_REGO_KEY);
17737
+ return { value: result.value, raw: result.raw };
16064
17738
  }
16065
17739
  async function modifyEnvAuthzUserRego(cloudbase, value) {
16066
17740
  if (!cloudbase?.permission?.modifyEnvAuthzConfig) {
16067
17741
  throw new Error("Current @cloudbase/manager-node does not expose permission.modifyEnvAuthzConfig. Upgrade manager-node (>= 5.5.5) to align with CLI tcb policy set.");
16068
17742
  }
17743
+ const validated = validateUserRegoContent(value);
16069
17744
  return cloudbase.permission.modifyEnvAuthzConfig({
16070
17745
  key: AUTHZ_USER_REGO_KEY,
16071
- value,
17746
+ value: validated,
16072
17747
  });
16073
17748
  }
17749
+ async function describeResourcePolicyListAligned(cloudbase, policyResourceType) {
17750
+ if (!cloudbase?.permission?.describeResourcePolicyList) {
17751
+ throw new Error("Current @cloudbase/manager-node does not expose permission.describeResourcePolicyList. Upgrade manager-node (>= 5.5.5) to align with CLI tcb policy list.");
17752
+ }
17753
+ return cloudbase.permission.describeResourcePolicyList(policyResourceType ? { resourceType: policyResourceType } : undefined);
17754
+ }
16074
17755
  async function describeResourcePermissionWithFunctionPgFallback(options) {
16075
17756
  const { cloudbase, envId, resourceType, resources } = options;
16076
17757
  try {
@@ -16304,7 +17985,7 @@ function registerPermissionTools(server) {
16304
17985
  };
16305
17986
  server.registerTool?.("queryPermissions", {
16306
17987
  title: "查询 CloudBase 权限与用户配置",
16307
- description: "查询 CloudBase 权限与用户配置,支持查询资源权限(数据库/云函数/存储桶等)、角色列表/详情、应用用户列表/详情。\n\n示例:\n- 查询存储桶权限:`action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\"`\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`。`resourceType=\"noSqlDatabase\"` 查询的是 CloudBase NoSQL 集合规则,与 CloudBase PostgreSQL(PG)表的行级安全(RLS)是两套独立机制——同一个 PG 环境里 NoSQL 集合若仍在使用,对那些集合查询本工具结果**仍然有效**。要查 PG 表 RLS,请改用 `queryPgDatabase(action=\"sql\", sql=\"SELECT * FROM pg_policies WHERE tablename=...\")`。本工具不涉及 MySQL 权限。\n\n⚠️ PostgreSQL 环境:平台 `DescribeResourcePermission` 对 PG 环境会直接拒绝。当 `resourceType=\"function\"` 时,本工具会自动回退到 Manager SDK `describeEnvAuthzConfig`(与 CLI `tcb policy get` 一致,读取 `authz.user.rego`)。",
17988
+ description: "查询 CloudBase 权限与用户配置,支持查询资源权限(数据库/云函数/存储桶等)、角色列表/详情、应用用户列表/详情,以及网关 OPA 授权策略(对齐 CLI `tcb policy list/get`)。\n\n示例:\n- 查询存储桶权限:`action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\"`\n- 列出旧网关策略:`action=\"listPolicy\"`(PG / OPA 引擎环境返回空列表,与 CLI 一致)\n- 读取用户 Rego:`action=\"getPolicy\"`;平台扩展策略:`action=\"getPolicy\", extension=true`\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`。`resourceType=\"noSqlDatabase\"` 查询的是 CloudBase NoSQL 集合规则,与 CloudBase PostgreSQL(PG)表的行级安全(RLS)是两套独立机制——同一个 PG 环境里 NoSQL 集合若仍在使用,对那些集合查询本工具结果**仍然有效**。要查 PG 表 RLS,请改用 `queryPgDatabase(action=\"sql\", sql=\"SELECT * FROM pg_policies WHERE tablename=...\")`。本工具不涉及 MySQL 权限。\n\n⚠️ PostgreSQL 环境:平台 `DescribeResourcePermission` 对 PG 环境会直接拒绝。当 `resourceType=\"function\"` 时,本工具会自动回退到 Manager SDK `describeEnvAuthzConfig`(与 CLI `tcb policy get` 一致,读取 `authz.user.rego`)。显式 OPA 策略请用 `listPolicy` / `getPolicy`。",
16308
17989
  inputSchema: {
16309
17990
  action: zod_1.z.enum(QUERY_PERMISSION_ACTIONS),
16310
17991
  resourceType: zod_1.z
@@ -16319,13 +18000,21 @@ function registerPermissionTools(server) {
16319
18000
  username: zod_1.z.string().optional(),
16320
18001
  pageNo: zod_1.z.number().optional(),
16321
18002
  pageSize: zod_1.z.number().optional(),
18003
+ extension: zod_1.z
18004
+ .boolean()
18005
+ .optional()
18006
+ .describe("仅 action=getPolicy。true=读取平台为该环境单独配置的策略(authz.platform.extension.rego),默认 false=用户策略(authz.user.rego),对齐 CLI `tcb policy get --extension`。"),
18007
+ policyResourceType: zod_1.z
18008
+ .enum(POLICY_LIST_RESOURCE_TYPES)
18009
+ .optional()
18010
+ .describe("仅 action=listPolicy。按资源类型过滤,当前仅支持 `policy`,对齐 CLI `tcb policy list --resource-type policy`。"),
16322
18011
  },
16323
18012
  annotations: {
16324
18013
  readOnlyHint: true,
16325
18014
  openWorldHint: true,
16326
18015
  category: "permissions",
16327
18016
  },
16328
- }, async ({ action, resourceType, resourceId, resourceIds, roleId, roleIdentity, roleName, uid, username, pageNo, pageSize, }) => withEnvelope(async () => {
18017
+ }, async ({ action, resourceType, resourceId, resourceIds, roleId, roleIdentity, roleName, uid, username, pageNo, pageSize, extension, policyResourceType, }) => withEnvelope(async () => {
16329
18018
  const envId = await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions);
16330
18019
  const cloudbase = await getManager();
16331
18020
  switch (action) {
@@ -16471,11 +18160,43 @@ function registerPermissionTools(server) {
16471
18160
  raw: result,
16472
18161
  }, "应用用户详情查询成功");
16473
18162
  }
18163
+ case "listPolicy": {
18164
+ const result = await describeResourcePolicyListAligned(cloudbase, policyResourceType);
18165
+ (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
18166
+ const data = result
18167
+ ?.Data;
18168
+ const policyList = data?.PolicyList ?? [];
18169
+ const total = data?.Total ?? policyList.length;
18170
+ return buildEnvelope({
18171
+ action,
18172
+ envId,
18173
+ policyResourceType: policyResourceType ?? null,
18174
+ policies: policyList,
18175
+ total,
18176
+ note: "PG 环境与 authz_engine=opa 的环境会返回空列表(与 CLI `tcb policy list` / SDK describeResourcePolicyList 一致)。读取用户 Rego 请用 action=getPolicy。",
18177
+ raw: result,
18178
+ }, "网关授权策略列表查询成功");
18179
+ }
18180
+ case "getPolicy": {
18181
+ const key = resolveAuthzConfigKey(extension);
18182
+ const result = await describeEnvAuthzConfigByKey(cloudbase, key);
18183
+ (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result.raw);
18184
+ return buildEnvelope({
18185
+ action,
18186
+ envId,
18187
+ key: result.key,
18188
+ extension: Boolean(extension),
18189
+ rego: result.value,
18190
+ raw: result.raw,
18191
+ }, extension
18192
+ ? "平台扩展 OPA 策略查询成功(authz.platform.extension.rego)"
18193
+ : "用户 OPA 策略查询成功(authz.user.rego)");
18194
+ }
16474
18195
  }
16475
18196
  }));
16476
18197
  server.registerTool?.("managePermissions", {
16477
18198
  title: "管理 CloudBase 权限与用户配置",
16478
- description: "管理 CloudBase 权限与用户配置,支持修改资源权限(数据库/云函数/存储桶等)、角色管理、成员与策略增删、应用用户 CRUD。\n\n示例:\n- 设置存储桶为私有:`action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"PRIVATE\"`\n- 创建角色:`action=\"createRole\", roleName=\"admin\", roleIdentity=\"admin\"`\n- 放开云函数匿名/未登录访问(PG 会走 OPA,对齐 CLI `tcb policy set`):`action=\"updateResourcePermission\", resourceType=\"function\", resourceId=\"myFn\", permission=\"CUSTOM\", securityRule='{\"invoke\":true}'`\n\n注意:`createUser` / `updateUser` 是环境侧应用用户管理能力,适合测试账号、管理员或预置用户,不应替代浏览器里的 Web SDK 注册表单;前端用户名密码注册应使用 `auth.signUp({ username, password })`,登录应使用 `auth.signInWithPassword({ username, password })`。直接在浏览器里用 `auth.signUp` 创建用户名密码用户取决于 SDK/provider 支持,使用前必须验证;不支持时应走后端或管理端边界,不能在浏览器暴露密钥。`securityRule` 的详细语义取决于 `resourceType`:`doc._openid`、`auth.openid`、查询条件子集校验,以及 `create` / `update` / `delete` JSON 模板仅适用于 `resourceType=\"noSqlDatabase\"` 的文档数据库安全规则;配置 `function` 或 `storage` 时,请参考各自官方安全规则文档,而不是复用 NoSQL 模板。\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`:\n- `resourceType=\"noSqlDatabase\"` 仅作用于 CloudBase NoSQL 文档数据库的集合;CloudBase PostgreSQL(PG)表的行级权限**不**受它控制——PG 表请改用 RLS:`managePgDatabase(action=\"execute\", confirm=true)` 跑 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` 与 `CREATE POLICY ...`。同一个 PG 环境里如果还有 NoSQL 集合在用,对那些**集合**继续使用 `noSqlDatabase` 规则是正确的——不是\"PG 环境就禁用本工具\"。\n- `resourceType=\"storage\"` 控制的是 NoSQL/COS 存储桶 ACL;PG 的 `pgstore` bucket 不在此 `resourceType` 覆盖范围内。\n- 本工具不涉及 MySQL;MySQL 数据库权限请走 MySQL 自身的 GRANT/REVOKE 语句(通过 `manageMysqlDatabase`)。\n\n⚠️ PostgreSQL 环境:平台 `ModifyResourcePermission` 对 PG 环境会直接拒绝。当 `resourceType=\"function\"` 时,本工具会自动回退到 Manager SDK `modifyEnvAuthzConfig`(与 CLI `tcb policy set` 一致,写入 `authz.user.rego`)。`securityRule` 可传完整 Rego(`package authz.user`)或 `'{\"invoke\":true}'`(自动生成放通 anonymous/unauthenticated 调 functions 的策略)。设置 Rego 后旧网关鉴权会失效,行为与 CLI 相同。",
18199
+ description: "管理 CloudBase 权限与用户配置,支持修改资源权限(数据库/云函数/存储桶等)、角色管理、成员与策略增删、应用用户 CRUD,以及设置网关 OPA Rego 策略(对齐 CLI `tcb policy set`)。\n\n示例:\n- 设置存储桶为私有:`action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"PRIVATE\"`\n- 创建角色:`action=\"createRole\", roleName=\"admin\", roleIdentity=\"admin\"`\n- 放开云函数匿名/未登录访问(PG 会走 OPA,对齐 CLI `tcb policy set`):`action=\"updateResourcePermission\", resourceType=\"function\", resourceId=\"myFn\", permission=\"CUSTOM\", securityRule='{\"invoke\":true}'`\n- 直接设置用户 Rego:`action=\"setPolicy\", regoContent=\"package authz.user\\n\\ndefault allow := false\\n\", confirm=true`(⚠️ 立即禁用旧网关鉴权)\n\n注意:`createUser` / `updateUser` 是环境侧应用用户管理能力,适合测试账号、管理员或预置用户,不应替代浏览器里的 Web SDK 注册表单;前端用户名密码注册应使用 `auth.signUp({ username, password })`,登录应使用 `auth.signInWithPassword({ username, password })`。直接在浏览器里用 `auth.signUp` 创建用户名密码用户取决于 SDK/provider 支持,使用前必须验证;不支持时应走后端或管理端边界,不能在浏览器暴露密钥。`securityRule` 的详细语义取决于 `resourceType`:`doc._openid`、`auth.openid`、查询条件子集校验,以及 `create` / `update` / `delete` JSON 模板仅适用于 `resourceType=\"noSqlDatabase\"` 的文档数据库安全规则;配置 `function` 或 `storage` 时,请参考各自官方安全规则文档,而不是复用 NoSQL 模板。\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`:\n- `resourceType=\"noSqlDatabase\"` 仅作用于 CloudBase NoSQL 文档数据库的集合;CloudBase PostgreSQL(PG)表的行级权限**不**受它控制——PG 表请改用 RLS:`managePgDatabase(action=\"execute\", confirm=true)` 跑 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` 与 `CREATE POLICY ...`。同一个 PG 环境里如果还有 NoSQL 集合在用,对那些**集合**继续使用 `noSqlDatabase` 规则是正确的——不是\"PG 环境就禁用本工具\"。\n- `resourceType=\"storage\"` 控制的是 NoSQL/COS 存储桶 ACL;PG 的 `pgstore` bucket 不在此 `resourceType` 覆盖范围内。\n- 本工具不涉及 MySQL;MySQL 数据库权限请走 MySQL 自身的 GRANT/REVOKE 语句(通过 `manageMysqlDatabase`)。\n\n⚠️ PostgreSQL 环境:平台 `ModifyResourcePermission` 对 PG 环境会直接拒绝。当 `resourceType=\"function\"` 时,本工具会自动回退到 Manager SDK `modifyEnvAuthzConfig`(与 CLI `tcb policy set` 一致,写入 `authz.user.rego`)。`securityRule` 可传完整 Rego(`package authz.user`)或 `'{\"invoke\":true}'`(自动生成放通 anonymous/unauthenticated 调 functions 的策略)。设置 Rego 后旧网关鉴权会失效,行为与 CLI 相同。显式 OPA 策略请优先用 `action=\"setPolicy\"`。",
16479
18200
  inputSchema: {
16480
18201
  action: zod_1.z.enum(MANAGE_PERMISSION_ACTIONS),
16481
18202
  resourceType: zod_1.z
@@ -16506,6 +18227,14 @@ function registerPermissionTools(server) {
16506
18227
  username: zod_1.z.string().optional(),
16507
18228
  password: zod_1.z.string().optional(),
16508
18229
  userStatus: zod_1.z.enum(["ACTIVE", "BLOCKED"]).optional(),
18230
+ regoContent: zod_1.z
18231
+ .string()
18232
+ .optional()
18233
+ .describe("仅 action=setPolicy。用户 OPA Rego 全文,必须以 `package authz.user` 开头,对齐 CLI `tcb policy set <regoContent>`。"),
18234
+ confirm: zod_1.z
18235
+ .boolean()
18236
+ .optional()
18237
+ .describe("仅 action=setPolicy。设置 Rego 后会立即禁用旧网关鉴权,必须显式传 confirm=true(对齐 CLI 确认提示)。"),
16509
18238
  },
16510
18239
  annotations: {
16511
18240
  readOnlyHint: false,
@@ -16514,7 +18243,7 @@ function registerPermissionTools(server) {
16514
18243
  openWorldHint: true,
16515
18244
  category: "permissions",
16516
18245
  },
16517
- }, async ({ action, resourceType, resourceId, permission, securityRule, roleId, roleIds, roleName, roleIdentity, description, memberUids, policies, policyIds, uid, uids, username, password, userStatus, }) => withEnvelope(async () => {
18246
+ }, async ({ action, resourceType, resourceId, permission, securityRule, roleId, roleIds, roleName, roleIdentity, description, memberUids, policies, policyIds, uid, uids, username, password, userStatus, regoContent, confirm, }) => withEnvelope(async () => {
16518
18247
  const envId = await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions);
16519
18248
  const cloudbase = await getManager();
16520
18249
  const normalizedPolicies = normalizeRecordArray(policies, "policies");
@@ -16695,6 +18424,26 @@ function registerPermissionTools(server) {
16695
18424
  raw: result,
16696
18425
  }, "应用用户删除成功");
16697
18426
  }
18427
+ case "setPolicy": {
18428
+ if (confirm !== true) {
18429
+ throw new Error("action=setPolicy 会立即禁用旧网关鉴权(对齐 CLI `tcb policy set`),必须显式传 confirm=true。");
18430
+ }
18431
+ const validated = validateUserRegoContent(regoContent ?? "");
18432
+ const result = await modifyEnvAuthzUserRego(cloudbase, validated);
18433
+ (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
18434
+ return buildEnvelope({
18435
+ action,
18436
+ envId,
18437
+ key: AUTHZ_USER_REGO_KEY,
18438
+ rego: validated,
18439
+ sideEffect: "Setting authz.user.rego immediately disables legacy gateway authorization.",
18440
+ nextSteps: [
18441
+ 'queryPermissions(action="getPolicy")',
18442
+ 'queryPermissions(action="listPolicy")',
18443
+ ],
18444
+ raw: result,
18445
+ }, "用户 OPA Rego 策略设置成功(旧网关鉴权已失效)");
18446
+ }
16698
18447
  }
16699
18448
  }));
16700
18449
  }
@@ -17185,7 +18934,7 @@ function registerSetupTools(server) {
17185
18934
  server.registerTool("downloadTemplate", {
17186
18935
  title: "下载项目模板",
17187
18936
  description: `自动下载并部署CloudBase项目模板。
17188
- **Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.26.0" : 0}),便于后续维护和版本追踪\n- 下载 rules 模板时,如果项目中已存在 README.md 文件,系统会自动保护该文件不被覆盖(除非设置 overwrite=true)`,
18937
+ **Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.28.0" : 0}),便于后续维护和版本追踪\n- 下载 rules 模板时,如果项目中已存在 README.md 文件,系统会自动保护该文件不被覆盖(除非设置 overwrite=true)`,
17189
18938
  inputSchema: {
17190
18939
  template: zod_1.z
17191
18940
  .enum(["react", "vue", "miniprogram", "uniapp", "rules"])
@@ -17541,7 +19290,9 @@ function registerAppTools(server) {
17541
19290
  buildId,
17542
19291
  });
17543
19292
  (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
17544
- const isFailed = result.Status === "FAILED";
19293
+ // Platform may return Failed/failed; normalize before matching.
19294
+ const isFailed = typeof result.Status === "string" &&
19295
+ result.Status.toLowerCase() === "failed";
17545
19296
  const payload = {
17546
19297
  action,
17547
19298
  serviceName,
@@ -17640,7 +19391,8 @@ function registerAppTools(server) {
17640
19391
  .optional()
17641
19392
  .describe("自定义部署命令。通常无需填写,默认自动生成 tcb hosting deploy 命令。" +
17642
19393
  "有 buildPath 时远端已 cd 到该目录,默认用 . 作为源码路径;无 buildPath 时默认用 dist。"),
17643
- ignore: zod_1.z.array(zod_1.z.string()).optional().describe("上传时忽略的文件/目录 glob 模式,例如 **/node_modules/**。"),
19394
+ ignore: zod_1.z.array(zod_1.z.string()).optional().describe("上传时忽略的文件/目录 glob 模式,例如 **/node_modules/**。\n" +
19395
+ "⚠️ 打包的是项目根目录(filePath)而非 buildPath 产物目录:若项目根含 target/(Rust)、.next/、dist-old/、build/ 等大构建产物,必须加进 ignore(如 **/target/**),否则整个目录被打进上传 zip(实证 54GB target → 34GB zip)。默认已排除 node_modules/.git/.DS_Store/**/target/**/.next/**/.next.bak/**。"),
17644
19396
  versionName: zod_1.z
17645
19397
  .string()
17646
19398
  .optional()
@@ -17660,6 +19412,17 @@ function registerAppTools(server) {
17660
19412
  if (!appService) {
17661
19413
  throw new Error("当前 manager 未提供 cloudAppService");
17662
19414
  }
19415
+ // 默认排除的大目录(2026-08-14 实证:ato 项目 target/ 54GB 被整个打进 zip)
19416
+ // tcb app deploy 打包的是项目根目录(localPath)而非 outputDir,必须排除构建产物
19417
+ const defaultPackIgnore = [
19418
+ "node_modules/**",
19419
+ ".git/**",
19420
+ ".DS_Store",
19421
+ "**/.DS_Store",
19422
+ "**/target/**",
19423
+ "**/.next/**",
19424
+ "**/.next.bak/**",
19425
+ ];
17663
19426
  // getUploadUrl — 获取预签名上传 URL(cloud mode 专用)
17664
19427
  if (action === "getUploadUrl") {
17665
19428
  if (!serviceName) {
@@ -17670,9 +19433,9 @@ function registerAppTools(server) {
17670
19433
  serviceName,
17671
19434
  });
17672
19435
  (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, cosInfoResult);
17673
- const defaultIgnore = ["node_modules/**", ".git/**", ".DS_Store", "**/.DS_Store"];
19436
+ const defaultIgnore = defaultPackIgnore;
17674
19437
  // eslint-disable-next-line max-len
17675
- const zipCmd = "zip -r upload.zip . -x 'node_modules/**' -x '.git/**' -x '.DS_Store' -x '**/.DS_Store'";
19438
+ const zipCmd = "zip -r upload.zip . -x 'node_modules/**' -x '.git/**' -x '.DS_Store' -x '**/.DS_Store' -x '**/target/**' -x '**/.next/**' -x '**/.next.bak/**'";
17676
19439
  const followupArgs = {
17677
19440
  action: "deployApp",
17678
19441
  serviceName,
@@ -17716,11 +19479,17 @@ function registerAppTools(server) {
17716
19479
  // 上传代码到 COS(仅本地模式需要,cloud mode 用 cosTimestamp 跳过)
17717
19480
  let cosTs = cosTimestamp;
17718
19481
  if (filePath && !cosTs) {
19482
+ // 默认排除大目录(2026-08-14 实证:ato 项目 target/ 54GB 被整个打进 zip)
19483
+ // 用户显式传 ignore 时合并,避免覆盖默认值
19484
+ const mergedIgnore = Array.from(new Set([
19485
+ ...defaultPackIgnore,
19486
+ ...(ignore ?? []),
19487
+ ]));
17719
19488
  const uploadResult = await appService.uploadCode({
17720
19489
  deployType: "static-hosting",
17721
19490
  serviceName,
17722
19491
  localPath: filePath,
17723
- ignore,
19492
+ ignore: mergedIgnore,
17724
19493
  });
17725
19494
  (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, uploadResult);
17726
19495
  cosTs = uploadResult.cosTimestamp;
@@ -19278,7 +21047,7 @@ exports.getLoginState = getLoginState;
19278
21047
  exports.logout = logout;
19279
21048
  const toolbox_1 = __webpack_require__(2090);
19280
21049
  const logger_js_1 = __webpack_require__(3039);
19281
- const tencent_cloud_js_1 = __webpack_require__(1454);
21050
+ const site_map_js_1 = __webpack_require__(8107);
19282
21051
  const auth = toolbox_1.AuthSupervisor.getInstance({});
19283
21052
  const authProgressState = {
19284
21053
  status: "IDLE",
@@ -19519,13 +21288,105 @@ function rejectAuthProgressState(error) {
19519
21288
  lastError: message,
19520
21289
  });
19521
21290
  }
19522
- function resetAuthProgressState() {
19523
- return updateAuthProgressState({
19524
- status: "IDLE",
19525
- authMode: undefined,
19526
- authChallenge: undefined,
19527
- lastError: undefined,
19528
- });
21291
+ function resetAuthProgressState() {
21292
+ return updateAuthProgressState({
21293
+ status: "IDLE",
21294
+ authMode: undefined,
21295
+ authChallenge: undefined,
21296
+ lastError: undefined,
21297
+ });
21298
+ }
21299
+ const LEGACY_SITE = "domestic";
21300
+ function isSlottedCredential(value) {
21301
+ if (!value || typeof value !== "object") {
21302
+ return false;
21303
+ }
21304
+ return "domestic" in value || "intl" in value;
21305
+ }
21306
+ async function readStoredCredentialRaw() {
21307
+ return toolbox_1.authStore.get("credential");
21308
+ }
21309
+ /**
21310
+ * 读取指定 site 槽位的原始凭证;旧格式(单槽 flat)视为 domestic 槽位。
21311
+ */
21312
+ async function readSiteCredentialRaw(site) {
21313
+ const raw = await readStoredCredentialRaw();
21314
+ if (!raw) {
21315
+ return undefined;
21316
+ }
21317
+ if (isSlottedCredential(raw)) {
21318
+ return raw[site];
21319
+ }
21320
+ return site === LEGACY_SITE ? raw : undefined;
21321
+ }
21322
+ /**
21323
+ * 将凭证写入指定 site 槽位,保留其他槽位数据。
21324
+ * base 缺省时读取当前存储;旧单槽数据会被迁移为 domestic 槽位(写回时升级分槽)。
21325
+ */
21326
+ async function writeSiteCredential(site, credential, base) {
21327
+ const current = base ?? (await readStoredCredentialRaw());
21328
+ const slotted = current && isSlottedCredential(current)
21329
+ ? { ...current }
21330
+ : { domestic: current ?? {} };
21331
+ slotted[site] = credential ?? {};
21332
+ await toolbox_1.authStore.set("credential", slotted);
21333
+ }
21334
+ /**
21335
+ * 确保存储已为分槽格式并返回槽位快照。
21336
+ * 用于登录前迁移旧单槽数据,避免 @cloudbase/toolbox 内部误命中旧凭证而跳过新站点登录。
21337
+ */
21338
+ async function ensureSlottedCredential() {
21339
+ const raw = await readStoredCredentialRaw();
21340
+ if (raw && isSlottedCredential(raw)) {
21341
+ return { ...raw };
21342
+ }
21343
+ const slotted = { domestic: raw ?? {} };
21344
+ await toolbox_1.authStore.set("credential", slotted);
21345
+ return slotted;
21346
+ }
21347
+ function isSlotTokenExpired(credential, gap = 120) {
21348
+ return (!!credential.accessTokenExpired &&
21349
+ Number(credential.accessTokenExpired) < Date.now() + gap * 1000);
21350
+ }
21351
+ /**
21352
+ * 读取并解析指定 site 的登录态,必要时走 refreshToken 续期(镜像 @cloudbase/toolbox 行为)。
21353
+ */
21354
+ async function resolveSiteLoginState(site) {
21355
+ const raw = await readSiteCredentialRaw(site);
21356
+ if (!raw) {
21357
+ return null;
21358
+ }
21359
+ const credential = (0, toolbox_1.resolveCredential)(raw);
21360
+ if (!credential?.secretId || !credential?.secretKey) {
21361
+ return null;
21362
+ }
21363
+ if (credential.refreshToken) {
21364
+ if (!isSlotTokenExpired(credential)) {
21365
+ return credential;
21366
+ }
21367
+ if (Date.now() < Number(credential.expired)) {
21368
+ try {
21369
+ const refreshed = await (0, toolbox_1.refreshTmpToken)(credential);
21370
+ await writeSiteCredential(site, refreshed ?? {});
21371
+ const resolved = (0, toolbox_1.resolveCredential)(refreshed ?? {});
21372
+ return resolved?.secretId ? resolved : null;
21373
+ }
21374
+ catch (e) {
21375
+ const code = e?.code;
21376
+ if (code === "AUTH_FAIL" || code === "InternalError.GetRoleError") {
21377
+ return null;
21378
+ }
21379
+ throw e;
21380
+ }
21381
+ }
21382
+ return null;
21383
+ }
21384
+ return credential;
21385
+ }
21386
+ function isUsableCredential(value) {
21387
+ return (!!value &&
21388
+ typeof value === "object" &&
21389
+ Object.keys(value).length > 0);
19529
21390
  }
19530
21391
  async function peekLoginState(options) {
19531
21392
  const envVarLoginState = normalizeLoginStateFromEnvVars(options);
@@ -19548,14 +21409,25 @@ async function peekLoginState(options) {
19548
21409
  (0, logger_js_1.debug)("loginByApiSecret");
19549
21410
  return envVarLoginState;
19550
21411
  }
19551
- // 尝试从 toolbox 本地存储读取(包括项目级 API Key 凭证)
19552
- return auth.getLoginState();
21412
+ // 按 site 读取分槽凭证(缺省按 TCB_SITE/region 解析站点,默认 domestic)
21413
+ const site = (0, site_map_js_1.normalizeSite)(options?.site) ?? (0, site_map_js_1.resolveSite)(options?.region, process.env.TCB_SITE);
21414
+ const slotLoginState = await resolveSiteLoginState(site);
21415
+ if (slotLoginState) {
21416
+ return slotLoginState;
21417
+ }
21418
+ // 兼容旧单槽数据(视为 domestic),走 toolbox 读取/续期
21419
+ if (site === LEGACY_SITE) {
21420
+ return auth.getLoginState();
21421
+ }
21422
+ return null;
19553
21423
  }
19554
21424
  async function ensureLogin(options) {
19555
21425
  (0, logger_js_1.debug)("TENCENTCLOUD_SECRETID", { hasSecretId: !!process.env.TENCENTCLOUD_SECRETID });
19556
21426
  (0, logger_js_1.debug)("CLOUDBASE_API_KEY", { hasApiKey: !!getCloudBaseApiKeyFromEnv() });
19557
21427
  const loginState = await peekLoginState({
19558
21428
  ignoreEnvVars: options?.ignoreEnvVars,
21429
+ site: options?.site,
21430
+ region: options?.region,
19559
21431
  });
19560
21432
  if (!loginState) {
19561
21433
  const resolvedAuthOptions = resolveAuthOptions({
@@ -19571,21 +21443,31 @@ async function ensureLogin(options) {
19571
21443
  throw new Error(validationError);
19572
21444
  }
19573
21445
  const mode = resolvedAuthOptions.authMode;
21446
+ // 按显式 site > TCB_SITE > region 映射表解析站点(ap-singapore 歧义默认 intl,兼容既有国际站行为)
21447
+ const resolvedSite = (0, site_map_js_1.resolveSite)(options?.region, options?.site ?? process.env.TCB_SITE);
19574
21448
  const loginOptions = { flow: mode };
19575
21449
  if (mode === "web") {
19576
21450
  loginOptions.getAuthUrl =
19577
- options?.fromCloudBaseLoginPage && !(0, tencent_cloud_js_1.isInternationalRegion)(options?.region)
21451
+ options?.fromCloudBaseLoginPage && resolvedSite === "domestic"
19578
21452
  ? (url) => {
19579
21453
  const separator = url.includes("?") ? "&" : "?";
19580
21454
  const urlWithParam = `${url}${separator}allowNoEnv=true`;
19581
- return `https://tcb.cloud.tencent.com/login?_redirect_uri=${encodeURIComponent(urlWithParam)}`;
21455
+ return `https://${site_map_js_1.SITE_REGION_MAP.domestic.authHost}/login?_redirect_uri=${encodeURIComponent(urlWithParam)}`;
19582
21456
  }
19583
21457
  : (url) => {
19584
- if ((0, tencent_cloud_js_1.isInternationalRegion)(options?.region)) {
19585
- url = url.replace("cloud.tencent.com", "tencentcloud.com");
21458
+ let finalUrl = url;
21459
+ if (resolvedSite === "intl") {
21460
+ try {
21461
+ const parsed = new URL(url);
21462
+ parsed.host = site_map_js_1.SITE_REGION_MAP.intl.authHost;
21463
+ finalUrl = parsed.toString();
21464
+ }
21465
+ catch {
21466
+ finalUrl = url.replace("cloud.tencent.com", "tencentcloud.com");
21467
+ }
19586
21468
  }
19587
- const separator = url.includes("?") ? "&" : "?";
19588
- return `${url}${separator}allowNoEnv=true`;
21469
+ const separator = finalUrl.includes("?") ? "&" : "?";
21470
+ return `${finalUrl}${separator}allowNoEnv=true`;
19589
21471
  };
19590
21472
  }
19591
21473
  else {
@@ -19606,8 +21488,23 @@ async function ensureLogin(options) {
19606
21488
  }
19607
21489
  }
19608
21490
  (0, logger_js_1.debug)("beforeloginByWebAuth", { loginOptions });
21491
+ // 登录前迁移旧单槽数据为分槽格式,避免 toolbox 内部命中旧凭证而跳过新站点登录
21492
+ const slotsBefore = await ensureSlottedCredential();
19609
21493
  try {
19610
- await auth.loginByWebAuth(loginOptions);
21494
+ const loginResult = await auth.loginByWebAuth(loginOptions);
21495
+ // toolbox 将新凭证写入 flat 'credential',将其并入对应 site 槽位,保留其他槽位
21496
+ const newRaw = await readStoredCredentialRaw();
21497
+ let credentialToSlot = loginResult;
21498
+ if (isSlottedCredential(newRaw)) {
21499
+ credentialToSlot = newRaw[resolvedSite] ?? newRaw.domestic;
21500
+ }
21501
+ else if (newRaw) {
21502
+ credentialToSlot = newRaw;
21503
+ }
21504
+ if (isUsableCredential(credentialToSlot)) {
21505
+ const merged = { ...slotsBefore, [resolvedSite]: credentialToSlot };
21506
+ await toolbox_1.authStore.set("credential", merged);
21507
+ }
19611
21508
  resolveAuthProgressState();
19612
21509
  }
19613
21510
  catch (error) {
@@ -19616,6 +21513,8 @@ async function ensureLogin(options) {
19616
21513
  }
19617
21514
  const loginState = await peekLoginState({
19618
21515
  ignoreEnvVars: options?.ignoreEnvVars,
21516
+ site: options?.site,
21517
+ region: options?.region,
19619
21518
  });
19620
21519
  (0, logger_js_1.debug)("loginByWebAuth", { mode, hasLoginState: !!loginState });
19621
21520
  return loginState;
@@ -19628,11 +21527,19 @@ async function ensureLogin(options) {
19628
21527
  async function getLoginState(options) {
19629
21528
  return ensureLogin(options);
19630
21529
  }
19631
- async function logout() {
19632
- const cwd = process.env.WORKSPACE_FOLDER_PATHS || process.cwd();
19633
- const result = await auth.logout({ cwd });
21530
+ async function logout(options) {
21531
+ const site = (0, site_map_js_1.normalizeSite)(options?.site) ?? "domestic";
21532
+ const raw = await readStoredCredentialRaw();
21533
+ if (raw && isSlottedCredential(raw)) {
21534
+ const slotted = { ...raw };
21535
+ delete slotted[site];
21536
+ await toolbox_1.authStore.set("credential", slotted);
21537
+ }
21538
+ else if (site === LEGACY_SITE) {
21539
+ const cwd = process.env.WORKSPACE_FOLDER_PATHS || process.cwd();
21540
+ await auth.logout({ cwd });
21541
+ }
19634
21542
  resetAuthProgressState();
19635
- return result;
19636
21543
  }
19637
21544
 
19638
21545
 
@@ -19657,6 +21564,126 @@ module.exports = __WEBPACK_EXTERNAL_MODULE_crypto__;
19657
21564
 
19658
21565
  module.exports = __WEBPACK_EXTERNAL_MODULE_fs_promises_f8dae9d1__;
19659
21566
 
21567
+ /***/ }),
21568
+
21569
+ /***/ 8107:
21570
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
21571
+
21572
+
21573
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
21574
+ exports.SITE_IDS = exports.SITE_REGION_MAP = void 0;
21575
+ exports.isSiteId = isSiteId;
21576
+ exports.normalizeSite = normalizeSite;
21577
+ exports.getSite = getSite;
21578
+ exports.resolveSite = resolveSite;
21579
+ exports.resolveSiteAndRegion = resolveSiteAndRegion;
21580
+ const project_config_js_1 = __webpack_require__(8445);
21581
+ exports.SITE_REGION_MAP = {
21582
+ domestic: {
21583
+ id: "domestic",
21584
+ label: "国内站",
21585
+ authHost: "tcb.cloud.tencent.com",
21586
+ consoleHost: "tcb.cloud.tencent.com",
21587
+ defaultRegion: "ap-shanghai",
21588
+ regions: ["ap-shanghai", "ap-guangzhou", "ap-singapore"],
21589
+ capabilities: { noSql: true },
21590
+ },
21591
+ intl: {
21592
+ id: "intl",
21593
+ label: "国际站",
21594
+ authHost: "tcb.tencentcloud.com",
21595
+ consoleHost: "tcb.tencentcloud.com",
21596
+ defaultRegion: "ap-singapore",
21597
+ regions: ["ap-singapore"],
21598
+ capabilities: { noSql: false },
21599
+ },
21600
+ };
21601
+ exports.SITE_IDS = Object.keys(exports.SITE_REGION_MAP);
21602
+ function isSiteId(value) {
21603
+ return value === "domestic" || value === "intl";
21604
+ }
21605
+ function normalizeSite(value) {
21606
+ if (typeof value !== "string") {
21607
+ return undefined;
21608
+ }
21609
+ const normalized = value.trim().toLowerCase();
21610
+ if (normalized === "intl" || normalized === "international") {
21611
+ return "intl";
21612
+ }
21613
+ if (normalized === "domestic" || normalized === "cn") {
21614
+ return "domestic";
21615
+ }
21616
+ return undefined;
21617
+ }
21618
+ /**
21619
+ * 根据 region 与显式 site 解析站点。
21620
+ *
21621
+ * - 显式 site 优先(`domestic`/`intl`,兼容 `cn`/`international` 别名)
21622
+ * - 否则查 SITE_REGION_MAP:region 只属于一个站点时返回该站点
21623
+ * - 当 region 同时存在于多个站点(如 ap-singapore)时返回 `ambiguous`,由上层决定
21624
+ * - 未知 region 或未提供 region 时回退 domestic(全局默认)
21625
+ */
21626
+ function getSite(region, explicitSite) {
21627
+ const explicit = normalizeSite(explicitSite);
21628
+ if (explicit) {
21629
+ return explicit;
21630
+ }
21631
+ if (!region) {
21632
+ return "domestic";
21633
+ }
21634
+ const matches = exports.SITE_IDS.filter((site) => exports.SITE_REGION_MAP[site].regions.includes(region));
21635
+ if (matches.length === 1) {
21636
+ return matches[0];
21637
+ }
21638
+ if (matches.length > 1) {
21639
+ return "ambiguous";
21640
+ }
21641
+ return "domestic";
21642
+ }
21643
+ /**
21644
+ * 认证/路由场景下解析站点:歧义(如 ap-singapore 未配 site)时默认 intl,保持既有国际站行为。
21645
+ */
21646
+ function resolveSite(region, explicitSite) {
21647
+ const site = getSite(region, explicitSite);
21648
+ return site === "ambiguous" ? "intl" : site;
21649
+ }
21650
+ /**
21651
+ * 统一 site/region 解析入口(收敛各调用点分散的 region fallback)。
21652
+ *
21653
+ * 优先级:显式参数 > 环境变量(TCB_SITE/TCB_REGION)> 项目配置(.cloudbase/project.json)> 全局默认
21654
+ * (site=domestic, region=ap-shanghai)。
21655
+ *
21656
+ * 当仅指定 region 且该 region 在多个 site 的地域列表中都存在(如 ap-singapore)时,
21657
+ * 返回 site=intl 并标记 ambiguous=true(兼容既有国际站行为),调用方可用 ambiguous 提示用户显式指定 site。
21658
+ */
21659
+ function resolveSiteAndRegion(opts = {}) {
21660
+ const projectConfig = (0, project_config_js_1.readProjectConfig)();
21661
+ const explicitSite = normalizeSite(opts.site) ??
21662
+ normalizeSite(process.env.TCB_SITE) ??
21663
+ normalizeSite(projectConfig?.site);
21664
+ const explicitRegion = opts.region ?? process.env.TCB_REGION ?? projectConfig?.region;
21665
+ let site = explicitSite;
21666
+ let ambiguous = false;
21667
+ if (!site && explicitRegion) {
21668
+ const inferred = getSite(explicitRegion);
21669
+ if (inferred === "ambiguous") {
21670
+ site = "intl";
21671
+ ambiguous = true;
21672
+ }
21673
+ else if (inferred !== "domestic") {
21674
+ site = inferred;
21675
+ }
21676
+ }
21677
+ const resolvedSite = site ?? "domestic";
21678
+ const region = explicitRegion ?? exports.SITE_REGION_MAP[resolvedSite].defaultRegion;
21679
+ return {
21680
+ site: resolvedSite,
21681
+ region,
21682
+ ...(ambiguous ? { ambiguous: true } : {}),
21683
+ };
21684
+ }
21685
+
21686
+
19660
21687
  /***/ }),
19661
21688
 
19662
21689
  /***/ 8116:
@@ -19879,418 +21906,38 @@ function listLikelyRedeployFields(config) {
19879
21906
 
19880
21907
  /***/ }),
19881
21908
 
19882
- /***/ 9097:
19883
- /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
21909
+ /***/ 8445:
21910
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
19884
21911
 
19885
21912
 
19886
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19887
- if (k2 === undefined) k2 = k;
19888
- var desc = Object.getOwnPropertyDescriptor(m, k);
19889
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19890
- desc = { enumerable: true, get: function() { return m[k]; } };
19891
- }
19892
- Object.defineProperty(o, k2, desc);
19893
- }) : (function(o, m, k, k2) {
19894
- if (k2 === undefined) k2 = k;
19895
- o[k2] = m[k];
19896
- }));
19897
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19898
- Object.defineProperty(o, "default", { enumerable: true, value: v });
19899
- }) : function(o, v) {
19900
- o["default"] = v;
19901
- });
19902
- var __importStar = (this && this.__importStar) || (function () {
19903
- var ownKeys = function(o) {
19904
- ownKeys = Object.getOwnPropertyNames || function (o) {
19905
- var ar = [];
19906
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
19907
- return ar;
19908
- };
19909
- return ownKeys(o);
19910
- };
19911
- return function (mod) {
19912
- if (mod && mod.__esModule) return mod;
19913
- var result = {};
19914
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
19915
- __setModuleDefault(result, mod);
19916
- return result;
19917
- };
19918
- })();
19919
21913
  Object.defineProperty(exports, "__esModule", ({ value: true }));
19920
- exports.registerDownloadTools = registerDownloadTools;
19921
- const crypto = __importStar(__webpack_require__(7823));
19922
- const fs = __importStar(__webpack_require__(4421));
19923
- const fsPromises = __importStar(__webpack_require__(7932));
19924
- const http = __importStar(__webpack_require__(3782));
19925
- const https = __importStar(__webpack_require__(3871));
19926
- const os = __importStar(__webpack_require__(8116));
19927
- const path = __importStar(__webpack_require__(2521));
19928
- const url_1 = __webpack_require__(6887);
19929
- const zod_1 = __webpack_require__(2971);
19930
- const remote_url_safety_js_1 = __webpack_require__(9192);
19931
- // 常量定义
19932
- const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
19933
- const MAX_REDIRECT_HOPS = 5;
19934
- const ALLOWED_CONTENT_TYPES = [
19935
- "text/",
19936
- "image/",
19937
- "application/json",
19938
- "application/xml",
19939
- "application/pdf",
19940
- "application/zip",
19941
- "application/x-zip-compressed"
19942
- ];
19943
- // 获取项目根目录
19944
- function getProjectRoot() {
19945
- // 优先级:环境变量 > 当前工作目录
19946
- return process.env.WORKSPACE_FOLDER_PATHS ||
19947
- process.env.PROJECT_ROOT ||
19948
- process.env.GITHUB_WORKSPACE ||
19949
- process.env.CI_PROJECT_DIR ||
19950
- process.env.BUILD_SOURCESDIRECTORY ||
19951
- process.cwd();
19952
- }
19953
- // 验证相对路径是否安全(不允许路径遍历)
19954
- function isPathSafe(relativePath) {
19955
- // 检查是否包含路径遍历操作
19956
- if (relativePath.includes('..') ||
19957
- relativePath.includes('~') ||
19958
- path.isAbsolute(relativePath)) {
19959
- return false;
19960
- }
19961
- // 检查路径是否规范化后仍然安全
19962
- const normalizedPath = path.normalize(relativePath);
19963
- if (normalizedPath.startsWith('..') ||
19964
- normalizedPath.startsWith('/') ||
19965
- normalizedPath.startsWith('\\')) {
19966
- return false;
19967
- }
19968
- return true;
19969
- }
19970
- // 计算最终下载路径
19971
- function calculateDownloadPath(relativePath) {
19972
- const projectRoot = getProjectRoot();
19973
- const finalPath = path.join(projectRoot, relativePath);
19974
- // 确保最终路径在项目根目录内
19975
- const normalizedProjectRoot = path.resolve(projectRoot);
19976
- const normalizedFinalPath = path.resolve(finalPath);
19977
- if (!normalizedFinalPath.startsWith(normalizedProjectRoot)) {
19978
- throw new Error('相对路径超出项目根目录范围');
19979
- }
19980
- return finalPath;
19981
- }
19982
- // 生成随机文件名
19983
- function generateRandomFileName(extension = '') {
19984
- const randomBytes = crypto.randomBytes(16);
19985
- const fileName = randomBytes.toString('hex');
19986
- return `${fileName}${extension}`;
19987
- }
19988
- // 获取安全的临时文件路径
19989
- function getSafeTempFilePath(fileName) {
19990
- return path.join(os.tmpdir(), fileName);
19991
- }
19992
- // 从 URL 或 Content-Disposition 获取文件扩展名
19993
- function getFileExtension(url, contentType, contentDisposition) {
19994
- let extension = "";
19995
- // 从 URL 获取扩展名
19996
- const urlPath = new url_1.URL(url).pathname;
19997
- const urlExt = path.extname(urlPath);
19998
- if (urlExt) {
19999
- extension = urlExt;
20000
- }
20001
- // 从 Content-Disposition 获取扩展名
20002
- if (contentDisposition) {
20003
- const filenameMatch = contentDisposition.match(/filename=["']?([^"']+)["']?/);
20004
- if (filenameMatch) {
20005
- const dispositionExt = path.extname(filenameMatch[1]);
20006
- if (dispositionExt) {
20007
- extension = dispositionExt;
20008
- }
20009
- }
20010
- }
20011
- // 从 Content-Type 获取扩展名
20012
- if (!extension && contentType) {
20013
- const mimeToExt = {
20014
- "text/plain": ".txt",
20015
- "text/html": ".html",
20016
- "text/css": ".css",
20017
- "text/javascript": ".js",
20018
- "image/jpeg": ".jpg",
20019
- "image/png": ".png",
20020
- "image/gif": ".gif",
20021
- "image/webp": ".webp",
20022
- "application/json": ".json",
20023
- "application/xml": ".xml",
20024
- "application/pdf": ".pdf",
20025
- "application/zip": ".zip",
20026
- "application/x-zip-compressed": ".zip"
20027
- };
20028
- extension = mimeToExt[contentType] || "";
20029
- }
20030
- return extension;
20031
- }
20032
- function isAllowedContentType(contentType) {
20033
- return ALLOWED_CONTENT_TYPES.some((allowedType) => contentType.startsWith(allowedType));
20034
- }
20035
- // 检查是否为可重试的网络错误
20036
- function isRetryableError(error) {
20037
- const retryableCodes = [
20038
- 'ECONNRESET',
20039
- 'ECONNREFUSED',
20040
- 'ETIMEDOUT',
20041
- 'ESOCKETTIMEDOUT',
20042
- 'ENOTFOUND',
20043
- 'EHOSTUNREACH',
20044
- 'ENETUNREACH',
20045
- 'ECONNABORTED'
20046
- ];
20047
- return retryableCodes.includes(error.code || '');
20048
- }
20049
- // 下载文件到指定路径(带重试逻辑)
20050
- async function downloadFileToPath(url, targetPath, retryCount = 0, redirectCount = 0) {
20051
- const MAX_RETRIES = 3;
20052
- const BASE_DELAY_MS = 1000; // 1秒基础延迟
20053
- if (redirectCount > MAX_REDIRECT_HOPS) {
20054
- throw new Error(`重定向次数超过 ${MAX_REDIRECT_HOPS} 次限制`);
20055
- }
20056
- const { requestUrl, requestOptions } = await (0, remote_url_safety_js_1.prepareSafeRemoteRequest)(url);
20057
- const client = requestUrl.protocol === "https:" ? https : http;
20058
- return new Promise((resolve, reject) => {
20059
- const request = client.get(requestUrl, requestOptions ?? {}, async (res) => {
20060
- // 处理重定向
20061
- if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
20062
- try {
20063
- const redirectUrl = new url_1.URL(res.headers.location, requestUrl).toString();
20064
- return resolve(await downloadFileToPath(redirectUrl, targetPath, retryCount, redirectCount + 1));
20065
- }
20066
- catch (error) {
20067
- return reject(error);
20068
- }
20069
- }
20070
- if (res.statusCode !== 200) {
20071
- reject(new Error(`HTTP Error: ${res.statusCode}`));
20072
- return;
20073
- }
20074
- const contentType = res.headers["content-type"] || "";
20075
- const contentLength = parseInt(res.headers["content-length"] || "0", 10);
20076
- if (!isAllowedContentType(contentType)) {
20077
- reject(new Error(`不允许的内容类型: ${contentType || "unknown"}`));
20078
- return;
20079
- }
20080
- // 文件大小检查
20081
- if (contentLength > MAX_FILE_SIZE) {
20082
- reject(new Error(`文件大小 ${contentLength} 字节超过 ${MAX_FILE_SIZE} 字节限制`));
20083
- return;
20084
- }
20085
- // 确保目标目录存在
20086
- const targetDir = path.dirname(targetPath);
20087
- try {
20088
- await fsPromises.mkdir(targetDir, { recursive: true });
20089
- }
20090
- catch (error) {
20091
- reject(new Error(`无法创建目标目录: ${error instanceof Error ? error.message : "未知错误"}`));
20092
- return;
20093
- }
20094
- // 创建写入流
20095
- const fileStream = fs.createWriteStream(targetPath);
20096
- let downloadedSize = 0;
20097
- res.on("data", (chunk) => {
20098
- downloadedSize += chunk.length;
20099
- if (downloadedSize > MAX_FILE_SIZE) {
20100
- fileStream.destroy();
20101
- fsPromises.unlink(targetPath).catch(() => { });
20102
- reject(new Error(`文件大小超过 ${MAX_FILE_SIZE} 字节限制`));
20103
- }
20104
- });
20105
- res.pipe(fileStream);
20106
- fileStream.on("finish", () => {
20107
- resolve({
20108
- filePath: targetPath,
20109
- contentType,
20110
- fileSize: downloadedSize,
20111
- });
20112
- });
20113
- fileStream.on("error", (error) => {
20114
- fsPromises.unlink(targetPath).catch(() => { });
20115
- reject(error);
20116
- });
20117
- });
20118
- request.on("error", async (error) => {
20119
- // 检查是否可重试
20120
- if (isRetryableError(error) && retryCount < MAX_RETRIES) {
20121
- const delay = BASE_DELAY_MS * Math.pow(2, retryCount); // 指数退避
20122
- console.warn(`下载失败 (${error.code}),${delay}ms 后重试 (${retryCount + 1}/${MAX_RETRIES})...`);
20123
- await new Promise((resolveDelay) => setTimeout(resolveDelay, delay));
20124
- try {
20125
- return resolve(await downloadFileToPath(url, targetPath, retryCount + 1, redirectCount));
20126
- }
20127
- catch (retryError) {
20128
- return reject(retryError);
20129
- }
20130
- }
20131
- reject(error);
20132
- });
20133
- });
20134
- }
20135
- // 下载文件到临时目录(保持向后兼容,带重试逻辑)
20136
- async function downloadFile(url, retryCount = 0, redirectCount = 0) {
20137
- const MAX_RETRIES = 3;
20138
- const BASE_DELAY_MS = 1000; // 1秒基础延迟
20139
- if (redirectCount > MAX_REDIRECT_HOPS) {
20140
- throw new Error(`重定向次数超过 ${MAX_REDIRECT_HOPS} 次限制`);
20141
- }
20142
- const { requestUrl, requestOptions } = await (0, remote_url_safety_js_1.prepareSafeRemoteRequest)(url);
20143
- const client = requestUrl.protocol === "https:" ? https : http;
20144
- return new Promise((resolve, reject) => {
20145
- const request = client.get(requestUrl, requestOptions ?? {}, async (res) => {
20146
- // 处理重定向
20147
- if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
20148
- try {
20149
- const redirectUrl = new url_1.URL(res.headers.location, requestUrl).toString();
20150
- return resolve(await downloadFile(redirectUrl, retryCount, redirectCount + 1));
20151
- }
20152
- catch (error) {
20153
- return reject(error);
20154
- }
20155
- }
20156
- if (res.statusCode !== 200) {
20157
- reject(new Error(`HTTP Error: ${res.statusCode}`));
20158
- return;
20159
- }
20160
- const contentType = res.headers["content-type"] || "";
20161
- const contentLength = parseInt(res.headers["content-length"] || "0", 10);
20162
- const contentDisposition = res.headers["content-disposition"];
20163
- if (!isAllowedContentType(contentType)) {
20164
- reject(new Error(`不允许的内容类型: ${contentType || "unknown"}`));
20165
- return;
20166
- }
20167
- // 文件大小检查
20168
- if (contentLength > MAX_FILE_SIZE) {
20169
- reject(new Error(`文件大小 ${contentLength} 字节超过 ${MAX_FILE_SIZE} 字节限制`));
20170
- return;
20171
- }
20172
- // 生成临时文件路径
20173
- const extension = getFileExtension(requestUrl.toString(), contentType, contentDisposition);
20174
- const fileName = generateRandomFileName(extension);
20175
- const filePath = getSafeTempFilePath(fileName);
20176
- // 创建写入流
20177
- const fileStream = fs.createWriteStream(filePath);
20178
- let downloadedSize = 0;
20179
- res.on("data", (chunk) => {
20180
- downloadedSize += chunk.length;
20181
- if (downloadedSize > MAX_FILE_SIZE) {
20182
- fileStream.destroy();
20183
- fsPromises.unlink(filePath).catch(() => { });
20184
- reject(new Error(`文件大小超过 ${MAX_FILE_SIZE} 字节限制`));
20185
- }
20186
- });
20187
- res.pipe(fileStream);
20188
- fileStream.on("finish", () => {
20189
- resolve({
20190
- filePath,
20191
- contentType,
20192
- fileSize: downloadedSize,
20193
- });
20194
- });
20195
- fileStream.on("error", (error) => {
20196
- fsPromises.unlink(filePath).catch(() => { });
20197
- reject(error);
20198
- });
20199
- });
20200
- request.on("error", async (error) => {
20201
- // 检查是否可重试
20202
- if (isRetryableError(error) && retryCount < MAX_RETRIES) {
20203
- const delay = BASE_DELAY_MS * Math.pow(2, retryCount); // 指数退避
20204
- console.warn(`下载失败 (${error.code}),${delay}ms 后重试 (${retryCount + 1}/${MAX_RETRIES})...`);
20205
- await new Promise((resolveDelay) => setTimeout(resolveDelay, delay));
20206
- try {
20207
- return resolve(await downloadFile(url, retryCount + 1, redirectCount));
20208
- }
20209
- catch (retryError) {
20210
- return reject(retryError);
20211
- }
20212
- }
20213
- reject(error);
20214
- });
20215
- });
20216
- }
20217
- function buildDownloadRemoteFileErrorMessage(url, error) {
20218
- const baseMessage = error instanceof Error ? error.message : String(error);
20219
- const suggestions = [];
20220
- if (/HTTP Error:\s*404/i.test(baseMessage)) {
20221
- suggestions.push("目标 URL 当前返回 404,说明资源链接已失效或路径不正确。");
20222
- suggestions.push("请先在浏览器中确认该链接可访问,再重新执行下载。");
20223
- }
20224
- if (/ENOTFOUND|ECONNRESET|ETIMEDOUT|socket hang up/i.test(baseMessage)) {
20225
- suggestions.push("下载过程中出现网络异常,系统已自动重试 3 次但均失败。");
20226
- suggestions.push("建议:1) 检查本地网络或代理设置;2) 该网站可能限频,请尝试更换其他图片源(如 Unsplash、Pexels);3) 稍后重试。");
20227
- }
20228
- if (suggestions.length === 0) {
20229
- suggestions.push("请检查 URL 是否可访问,以及目标资源是否允许直接下载。");
20230
- }
20231
- return `[downloadRemoteFile] ${baseMessage}\nURL: ${url}\n建议:${suggestions.join(" ")}`;
20232
- }
20233
- function registerDownloadTools(server) {
20234
- server.registerTool("downloadRemoteFile", {
20235
- title: "下载远程文件到指定路径",
20236
- description: "下载远程文件到项目根目录下的指定相对路径。例如:小程序的 Tabbar 等素材图片,必须使用 **png** 格式,可以从 Unsplash、wikimedia【一般选用 500 大小即可、Pexels、Apple 官方 UI 等资源中选择来下载。",
20237
- inputSchema: {
20238
- url: zod_1.z.string().describe("远程文件的 URL 地址"),
20239
- relativePath: zod_1.z.string().describe("相对于项目根目录的路径,例如:'assets/images/logo.png' 或 'docs/api.md'。不允许使用 ../ 等路径遍历操作。")
20240
- },
20241
- annotations: {
20242
- readOnlyHint: false,
20243
- destructiveHint: false,
20244
- idempotentHint: false,
20245
- openWorldHint: true,
20246
- category: "download"
20247
- }
20248
- }, async ({ url, relativePath }) => {
20249
- if (!isPathSafe(relativePath)) {
20250
- return {
20251
- content: [
20252
- {
20253
- type: "text",
20254
- text: JSON.stringify({
20255
- success: false,
20256
- error: "不安全的相对路径",
20257
- message: "相对路径包含路径遍历操作(../)或绝对路径,出于安全考虑已拒绝",
20258
- suggestion: "请使用项目根目录下的相对路径,例如:'assets/images/logo.png'"
20259
- }, null, 2)
20260
- }
20261
- ]
20262
- };
20263
- }
20264
- const targetPath = calculateDownloadPath(relativePath);
20265
- const projectRoot = getProjectRoot();
20266
- console.log(`📁 项目根目录: ${projectRoot}`);
20267
- console.log(`📁 相对路径: ${relativePath}`);
20268
- console.log(`📁 最终路径: ${targetPath}`);
20269
- let result;
20270
- try {
20271
- result = await downloadFileToPath(url, targetPath);
21914
+ exports.readProjectConfig = readProjectConfig;
21915
+ const node_fs_1 = __webpack_require__(9627);
21916
+ const node_path_1 = __webpack_require__(5663);
21917
+ /**
21918
+ * 读取项目级配置 `.cloudbase/project.json`。
21919
+ *
21920
+ * 该文件是 site/region/envId 的可选持久化来源,让"打开项目即连对服务"
21921
+ * (对齐 Vercel `.vercel/project.json` 的三级回退)。读取失败/文件不存在
21922
+ * 返回 undefined,不阻塞调用方。
21923
+ */
21924
+ function readProjectConfig(cwd) {
21925
+ try {
21926
+ const projectRoot = cwd ?? process.env.WORKSPACE_FOLDER_PATHS ?? process.cwd();
21927
+ const configPath = (0, node_path_1.join)(projectRoot, ".cloudbase", "project.json");
21928
+ if (!(0, node_fs_1.existsSync)(configPath)) {
21929
+ return undefined;
20272
21930
  }
20273
- catch (error) {
20274
- throw new Error(buildDownloadRemoteFileErrorMessage(url, error));
21931
+ const raw = (0, node_fs_1.readFileSync)(configPath, "utf-8");
21932
+ const parsed = JSON.parse(raw);
21933
+ if (!parsed || typeof parsed !== "object") {
21934
+ return undefined;
20275
21935
  }
20276
- return {
20277
- content: [
20278
- {
20279
- type: "text",
20280
- text: JSON.stringify({
20281
- success: true,
20282
- filePath: result.filePath,
20283
- relativePath: relativePath,
20284
- contentType: result.contentType,
20285
- fileSize: result.fileSize,
20286
- projectRoot: projectRoot,
20287
- message: "文件下载成功到指定路径",
20288
- note: `文件已保存到项目目录: ${relativePath}`
20289
- }, null, 2)
20290
- }
20291
- ]
20292
- };
20293
- });
21936
+ return parsed;
21937
+ }
21938
+ catch {
21939
+ return undefined;
21940
+ }
20294
21941
  }
20295
21942
 
20296
21943
 
@@ -20985,6 +22632,13 @@ function registerAgentTools(server) {
20985
22632
  }
20986
22633
 
20987
22634
 
22635
+ /***/ }),
22636
+
22637
+ /***/ 9627:
22638
+ /***/ ((module) => {
22639
+
22640
+ module.exports = __WEBPACK_EXTERNAL_MODULE_node_fs_75ed2103__;
22641
+
20988
22642
  /***/ }),
20989
22643
 
20990
22644
  /***/ 9684:
@@ -21059,7 +22713,6 @@ function shouldRegisterTool(toolName) {
21059
22713
  'createFunction', // also involves local files
21060
22714
  // Download tools - local file downloads
21061
22715
  'downloadTemplate',
21062
- 'downloadRemoteFile',
21063
22716
  // Setup tools - local config file operations
21064
22717
  'setupEnvironmentId',
21065
22718
  // CloudRun tools - local file operations