rainskills 0.1.26 → 0.1.28

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.
Files changed (90) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +5 -1
  3. package/bin/rainskills-tools.js +67 -7
  4. package/bin/rainskills.js +24 -5
  5. package/install.sh +24 -0
  6. package/marketplace/rainskills/.claude-plugin/plugin.json +1 -1
  7. package/marketplace/rainskills/.codex-plugin/plugin.json +1 -1
  8. package/marketplace/rainskills/skills/rainskills/SKILL.md +6 -2
  9. package/package.json +3 -2
  10. package/rainbond-app-assistant/SKILL.md +3 -1
  11. package/rainbond-app-assistant/evals/01-linked-topology-missing.expected.yaml +1 -0
  12. package/rainbond-app-assistant/evals/02-linked-topology-building.expected.yaml +1 -0
  13. package/rainbond-app-assistant/evals/03-runtime-healthy-delivery-unknown.expected.yaml +1 -0
  14. package/rainbond-app-assistant/evals/04-delivered-stop-without-promotion.expected.yaml +1 -1
  15. package/rainbond-app-assistant/evals/05-delivered-explicit-dev-to-test.expected.yaml +1 -0
  16. package/rainbond-app-assistant/evals/06-manual-validation-stop.expected.yaml +1 -0
  17. package/rainbond-app-assistant/evals/07-cluster-capacity-blocked.expected.yaml +1 -0
  18. package/rainbond-app-assistant/evals/08-code-build-handoff-stop.expected.yaml +1 -0
  19. package/rainbond-app-assistant/evals/09-source-backed-failure-no-fallback.expected.yaml +1 -0
  20. package/rainbond-app-assistant/evals/10-identity-ambiguous-stop.expected.yaml +1 -0
  21. package/rainbond-app-assistant/evals/11-delivered-with-proxy-and-local-binding.expected.yaml +1 -1
  22. package/rainbond-app-assistant/evals/12-complex-suite-needs-descriptor-stop.expected.yaml +1 -0
  23. package/rainbond-app-assistant/evals/13-explicit-git-url-locks-source.expected.yaml +1 -0
  24. package/rainbond-app-assistant/evals/14-strategy-switch-cleans-residue.expected.yaml +1 -0
  25. package/rainbond-app-assistant/evals/15-turn-discipline-reuse-alias.expected.yaml +1 -0
  26. package/rainbond-app-assistant/evals/16-source-delivered-manual-validation.expected.yaml +1 -1
  27. package/rainbond-app-assistant/references/operational-reference.md +1 -1
  28. package/rainbond-app-assistant/references/output-contract.md +8 -23
  29. package/rainbond-app-assistant/references/runtime-gate.md +6 -2
  30. package/rainbond-app-assistant/references/workflow-rules.md +1 -1
  31. package/rainbond-app-assistant/scripts/validate_app_assistant_output.py +10 -66
  32. package/rainbond-app-version-assistant/SKILL.md +17 -4
  33. package/rainbond-delivery-verifier/SKILL.md +5 -1
  34. package/rainbond-delivery-verifier/evals/01-delivered-verified.expected.yaml +1 -0
  35. package/rainbond-delivery-verifier/evals/02-delivered-manual-validation.expected.yaml +1 -0
  36. package/rainbond-delivery-verifier/evals/03-partially-delivered-building.expected.yaml +1 -0
  37. package/rainbond-delivery-verifier/evals/04-blocked-capacity.expected.yaml +1 -0
  38. package/rainbond-delivery-verifier/evals/05-blocked-no-usable-url.expected.yaml +1 -0
  39. package/rainbond-delivery-verifier/evals/06-blocked-runtime-unhealthy.expected.yaml +1 -0
  40. package/rainbond-delivery-verifier/scripts/validate_delivery_verifier_output.py +12 -0
  41. package/rainbond-env-sync/SKILL.md +17 -4
  42. package/rainbond-fullstack-bootstrap/SKILL.md +43 -4
  43. package/rainbond-fullstack-bootstrap/evals/01-reuse-only.expected.yaml +1 -0
  44. package/rainbond-fullstack-bootstrap/evals/02-deferred-dependency.expected.yaml +1 -0
  45. package/rainbond-fullstack-bootstrap/evals/03-capacity-blocked.expected.yaml +1 -0
  46. package/rainbond-fullstack-bootstrap/evals/04-template-skip.expected.yaml +1 -0
  47. package/rainbond-fullstack-bootstrap/evals/05-source-ambiguity-stop.expected.yaml +1 -0
  48. package/rainbond-fullstack-bootstrap/evals/06-external-artifact-unreachable.expected.yaml +1 -0
  49. package/rainbond-fullstack-bootstrap/evals/07-provider-connection-contract.expected.yaml +1 -0
  50. package/rainbond-fullstack-bootstrap/evals/08-explicit-dependency-after-manual-components.expected.yaml +1 -0
  51. package/rainbond-fullstack-bootstrap/evals/09-inferred-provider-consumer-gate.expected.yaml +1 -0
  52. package/rainbond-fullstack-bootstrap/evals/10-compose-service-name-not-hostname.expected.yaml +1 -0
  53. package/rainbond-fullstack-bootstrap/evals/11-reverse-proxy-not-dropped.expected.yaml +1 -0
  54. package/rainbond-fullstack-bootstrap/evals/12-optional-services-disclose-not-deploy.expected.yaml +1 -0
  55. package/rainbond-fullstack-bootstrap/evals/13-env-reference-dependency-completeness.expected.yaml +1 -0
  56. package/rainbond-fullstack-bootstrap/evals/14-client-package-upload.expected.yaml +1 -0
  57. package/rainbond-fullstack-bootstrap/evals/15-unfinished-dockerfile-recovery.expected.yaml +1 -0
  58. package/rainbond-fullstack-bootstrap/evals/16-complete-cnb-recreate-confirmed.expected.yaml +1 -0
  59. package/rainbond-fullstack-bootstrap/evals/17-existing-app-mysql-preflight-stop.expected.yaml +29 -0
  60. package/rainbond-fullstack-bootstrap/evals/17-existing-app-mysql-preflight-stop.prompt.md +1 -0
  61. package/rainbond-fullstack-bootstrap/evals/17-existing-app-mysql-preflight-stop.response.md +42 -0
  62. package/rainbond-fullstack-bootstrap/modules/30-creation-rules.md +4 -1
  63. package/rainbond-fullstack-bootstrap/modules/70-output-contract.md +2 -2
  64. package/rainbond-fullstack-bootstrap/references/quick-reference.md +2 -2
  65. package/rainbond-fullstack-bootstrap/scripts/validate_bootstrap_output.py +12 -0
  66. package/rainbond-fullstack-troubleshooter/SKILL.md +5 -1
  67. package/rainbond-fullstack-troubleshooter/evals/01-source-build-failed.expected.yaml +1 -0
  68. package/rainbond-fullstack-troubleshooter/evals/02-source-build-still-running.expected.yaml +1 -0
  69. package/rainbond-fullstack-troubleshooter/evals/03-dependency-missing.expected.yaml +1 -0
  70. package/rainbond-fullstack-troubleshooter/evals/04-wrong-connection-values.expected.yaml +1 -0
  71. package/rainbond-fullstack-troubleshooter/evals/05-env-naming-incompatibility.expected.yaml +1 -0
  72. package/rainbond-fullstack-troubleshooter/evals/06-frontend-access-path-issue.expected.yaml +1 -0
  73. package/rainbond-fullstack-troubleshooter/evals/07-cluster-capacity-blocked.expected.yaml +1 -0
  74. package/rainbond-fullstack-troubleshooter/evals/08-code-or-build-handoff-needed.expected.yaml +1 -0
  75. package/rainbond-fullstack-troubleshooter/evals/09-external-artifact-unreachable.expected.yaml +1 -0
  76. package/rainbond-fullstack-troubleshooter/evals/10-config-file-configmap-missing.expected.yaml +1 -0
  77. package/rainbond-fullstack-troubleshooter/references/output-contract.md +3 -3
  78. package/rainbond-fullstack-troubleshooter/scripts/validate_troubleshoot_output.py +12 -0
  79. package/rainbond-opensource-app-deploy/SKILL.md +1 -1
  80. package/rainbond-opensource-app-deploy/references/runtime-gate.md +5 -1
  81. package/rainbond-platform-installer/SKILL.md +1 -1
  82. package/rainbond-platform-installer/scripts/installed-version.js +1 -1
  83. package/rainbond-platform-installer/scripts/runtime-state.js +4 -2
  84. package/rainbond-platform-installer/scripts/windows-client-config.js +68 -43
  85. package/rainbond-platform-query/SKILL.md +5 -1
  86. package/rainbond-project-init/SKILL.md +21 -8
  87. package/rainbond-project-init/references/operational-reference.md +1 -1
  88. package/rainbond-project-init/references/output-contract.md +6 -6
  89. package/rainbond-template-installer/SKILL.md +17 -4
  90. package/scripts/validate_customer_output.py +59 -0
package/README.md CHANGED
@@ -223,7 +223,7 @@ node ~/.rainbond/lib/rainskills/bin/rainskills.js platform install --onboarding-
223
223
 
224
224
  Rainskills 会在用户下一次发起业务动作时,由本地运行时立即返回环境查询结果,并另行启动后台任务静默检查更新。更新只跟随 npm `latest` 指向的正式版。当前版本是 RC 或其他预发布版本时不会查询、不会自动升级;npm 上的新 RC 版本也不参与正式版自动升级。
225
225
 
226
- 发现更高的正式版后,后台任务只委托到经过校验的精确版本,例如 `rainskills@0.1.26`,不会执行浮动的 `@latest` 业务代码。新版本原子刷新已经安装的 Rainskills Skills;当前业务继续使用启动时已经加载的版本,最迟从下一条新任务开始使用新版。npm 超时、版本检查失败、安装位置不安全或文件迁移失败时会保留旧版本,且不会阻塞或改变当前操作。
226
+ 发现更高的正式版后,后台任务只委托到经过校验的精确版本,例如 `rainskills@0.1.28`,不会执行浮动的 `@latest` 业务代码。新版本原子刷新已经安装的 Rainskills Skills;当前业务继续使用启动时已经加载的版本,最迟从下一条新任务开始使用新版。npm 超时、版本检查失败、安装位置不安全或文件迁移失败时会保留旧版本,且不会阻塞或改变当前操作。
227
227
 
228
228
  升级只更新 Rainskills 自身,不触发 Rainbond 安装、运行环境选择、登录授权或重新对接,也不会新增 Agent MCP 配置。更新内容仅包括 Skills 和本地 CLI。原始业务操作会继续执行;只有该业务操作本身需要运行环境时,才按既有门禁检查当前连接。可用连接直接复用,401 只重新授权一次,403 立即停止,从未连接过运行环境时才进入环境选择。
229
229
 
package/SKILL.md CHANGED
@@ -44,7 +44,11 @@ Rainskills 安装完成,下一条消息即可直接使用。
44
44
 
45
45
  `runtime connect` 的 Device Flow 不依赖 stdin TTY;Agent 必须执行固定 argv 并保持进程附着直到授权完成。能打开本机浏览器时由连接器自动跳转,SSH、容器等无浏览器场景原样展示授权地址并继续轮询。只有 Rainbond 不支持 Device Flow 且进入旧版 loopback 手动粘贴时才需要交互终端;不得要求用户在聊天中粘贴 JWT。
46
46
 
47
- Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.26`):
47
+ 授权命令是同步门禁。执行工具返回“进程仍在运行”或会话 ID 时,必须只等待或轮询同一个命令会话;在该会话结束前,禁止读取专项 Skill、解析 context、调用业务 CLI 或执行任何后续业务步骤。浏览器页面显示成功不代表连接完成;只有原命令退出码为 0,并输出 `rainskills.runtime-connect-result.v1` 且 `state=connected`,才可继续。不得另起 `runtime status` 猜测完成,也不得重复提示用户授权。
48
+
49
+ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id` 反复调用 `write_stdin`(空输入轮询),直到工具返回 `exit_code`。连接器输出 `[RAINSKILLS_AGENT_WAIT_REQUIRED:runtime-connect]` 后进入上述轮询;看到 `[RAINSKILLS_AGENT_WAIT_COMPLETE:runtime-connect]` 后仍须继续轮询,直到取得退出码和最终 JSON。
50
+
51
+ Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.28`):
48
52
 
49
53
  - 状态:执行 `runtime status --json`。
50
54
  - 首次连接:执行 `runtime connect <target> --saas` 或 `runtime connect <target> --rainbond-url <Console origin>`。
@@ -29,6 +29,9 @@ const CONTEXT_HINT_FIELDS = new Set(["team_id", "team_name", "region_name"]);
29
29
  const SKILL_CONTENT_MAX_BYTES = 128 * 1024;
30
30
  const SKILL_MANIFEST_MAX_BYTES = 4 * 1024 * 1024;
31
31
  const SENSITIVE_RESPONSE_KEY_PATTERN = /(?:authorization|jwt|token|password|secret|credential|private[_-]?key|key[_-]?file|certificate|cert[_-]?file|ssl[_-]?ca[_-]?cert)/i;
32
+ const SENSITIVE_ENV_NAME_PATTERN = /(?:^|_)(?:PASSWORD|PASSWD|PASS|SECRET|TOKEN|API_?KEY|PRIVATE_?KEY|CREDENTIALS?|AUTH(?:ORIZATION)?|COOKIE|CERT(?:IFICATE)?)(?:_|$)/i;
33
+ const ENV_NAME_FIELDS = Object.freeze(["attr_name", "name", "note", "env_name"]);
34
+ const ENV_VALUE_FIELDS = new Set(["attr_value", "value"]);
32
35
  const RISK_POLICY = Object.freeze({
33
36
  version: "1",
34
37
  readPrefixes: [
@@ -37,6 +40,12 @@ const RISK_POLICY = Object.freeze({
37
40
  ],
38
41
  destructiveFragments: ["delete", "remove", "purge", "destroy"],
39
42
  });
43
+ const MIXED_TOOL_OPERATION_POLICY = Object.freeze({
44
+ rainbond_manage_component_dependency: Object.freeze({
45
+ readOperations: Object.freeze(["summary"]),
46
+ destructiveOperations: Object.freeze(["delete"]),
47
+ }),
48
+ });
40
49
  const CALL_OPTION_DEFINITIONS = Object.freeze({
41
50
  "--confirm": Object.freeze({ property: "confirmation", pattern: OPERATION_ID_PATTERN }),
42
51
  "--skill-id": Object.freeze({ property: "skillId", pattern: SKILL_ID_PATTERN }),
@@ -489,8 +498,16 @@ function loadSkillBinding(config, skillId, rootSkillId) {
489
498
  };
490
499
  }
491
500
 
492
- function classifyTool(toolName) {
501
+ function classifyTool(toolName, argumentsValue = {}) {
493
502
  const normalized = String(toolName || "").toLowerCase();
503
+ const operation = typeof argumentsValue.operation === "string"
504
+ ? argumentsValue.operation.trim().toLowerCase()
505
+ : "";
506
+ const mixedPolicy = MIXED_TOOL_OPERATION_POLICY[normalized];
507
+ if (mixedPolicy) {
508
+ if (mixedPolicy.destructiveOperations.includes(operation)) return "destructive";
509
+ if (mixedPolicy.readOperations.includes(operation)) return "read";
510
+ }
494
511
  if (RISK_POLICY.destructiveFragments.some((fragment) => normalized.includes(fragment))) {
495
512
  return "destructive";
496
513
  }
@@ -679,12 +696,47 @@ function fitOutput(value) {
679
696
  };
680
697
  }
681
698
 
682
- function sanitizeToolResult(value, pathParts = []) {
699
+ function redactString(value, secrets) {
700
+ let safe = value;
701
+ for (const secret of secrets) {
702
+ if (secret) safe = safe.split(secret).join("[REDACTED]");
703
+ }
704
+ return safe;
705
+ }
706
+
707
+ function hasSensitiveEnvName(value) {
708
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
709
+ return ENV_NAME_FIELDS.some((field) => (
710
+ typeof value[field] === "string" && SENSITIVE_ENV_NAME_PATTERN.test(value[field])
711
+ ));
712
+ }
713
+
714
+ function collectSensitiveArgumentRedactions(value, output = []) {
715
+ if (Array.isArray(value)) {
716
+ for (const item of value) collectSensitiveArgumentRedactions(item, output);
717
+ return output;
718
+ }
719
+ if (!value || typeof value !== "object") return output;
720
+
721
+ const sensitiveEnv = hasSensitiveEnvName(value);
722
+ for (const [key, item] of Object.entries(value)) {
723
+ const sensitiveField = SENSITIVE_RESPONSE_KEY_PATTERN.test(key)
724
+ || SENSITIVE_ENV_NAME_PATTERN.test(key)
725
+ || (sensitiveEnv && ENV_VALUE_FIELDS.has(key));
726
+ if (sensitiveField && typeof item === "string" && item.length >= 4) output.push(item);
727
+ collectSensitiveArgumentRedactions(item, output);
728
+ }
729
+ return output;
730
+ }
731
+
732
+ function sanitizeToolResult(value, pathParts = [], sensitiveArguments = []) {
683
733
  if (Array.isArray(value)) {
684
- return value.map((item) => sanitizeToolResult(item, pathParts));
734
+ return value.map((item) => sanitizeToolResult(item, pathParts, sensitiveArguments));
685
735
  }
736
+ if (typeof value === "string") return redactString(value, sensitiveArguments);
686
737
  if (!value || typeof value !== "object") return value;
687
738
  const safe = {};
739
+ const sensitiveEnv = hasSensitiveEnvName(value);
688
740
  for (const [key, item] of Object.entries(value)) {
689
741
  // Console destructive operations may return this one-time control value.
690
742
  // It is not a credential and must remain available for the confirmation flow.
@@ -695,7 +747,11 @@ function sanitizeToolResult(value, pathParts = []) {
695
747
  if (key !== "confirmation_token"
696
748
  && !isPackageUploadAuthMode
697
749
  && SENSITIVE_RESPONSE_KEY_PATTERN.test(key)) continue;
698
- safe[key] = sanitizeToolResult(item, [...pathParts, key]);
750
+ if (sensitiveEnv && ENV_VALUE_FIELDS.has(key)) {
751
+ safe[key] = "[REDACTED]";
752
+ continue;
753
+ }
754
+ safe[key] = sanitizeToolResult(item, [...pathParts, key], sensitiveArguments);
699
755
  }
700
756
  return safe;
701
757
  }
@@ -1205,14 +1261,14 @@ async function execute(command, config) {
1205
1261
  };
1206
1262
  }
1207
1263
 
1208
- const operationClass = classifyTool(command.toolName);
1264
+ const argumentsValue = command.argumentsValue || readArguments(command.input);
1265
+ const operationClass = classifyTool(command.toolName, argumentsValue);
1209
1266
  if (command.command === "read" && operationClass !== "read") {
1210
1267
  throw new BridgeError(
1211
1268
  "read is read-only and rejects tools classified as write or destructive",
1212
1269
  EXIT.USAGE
1213
1270
  );
1214
1271
  }
1215
- const argumentsValue = command.argumentsValue || readArguments(command.input);
1216
1272
  let operation = null;
1217
1273
  let skillBinding = null;
1218
1274
  if (operationClass !== "read") {
@@ -1274,7 +1330,11 @@ async function execute(command, config) {
1274
1330
  throw new BridgeError("Rainbond tool response lacks structuredContent", EXIT.TRANSPORT);
1275
1331
  }
1276
1332
  if (operation) updateOperation(config, operation, "succeeded");
1277
- return fitOutput(sanitizeToolResult(result.structuredContent));
1333
+ return fitOutput(sanitizeToolResult(
1334
+ result.structuredContent,
1335
+ [],
1336
+ collectSensitiveArgumentRedactions(argumentsValue)
1337
+ ));
1278
1338
  } catch (error) {
1279
1339
  if (operation) {
1280
1340
  updateOperation(config, operation, error.exitCode === EXIT.TRANSPORT ? "unknown" : "failed");
package/bin/rainskills.js CHANGED
@@ -167,11 +167,29 @@ function runtimeConnectRetryAction(options, origin) {
167
167
  };
168
168
  }
169
169
 
170
- function runAttached(executable, args, { env = process.env } = {}) {
170
+ function runAttached(executable, args, {
171
+ env = process.env,
172
+ signalSource = process,
173
+ spawnImpl = spawn,
174
+ } = {}) {
171
175
  return new Promise((resolve, reject) => {
172
- const child = spawn(executable, args, { env, stdio: "inherit" });
173
- child.once("error", () => reject(new Error("无法启动 RainSkills 运行环境连接器")));
174
- child.once("close", (code, signal) => resolve({ code, signal }));
176
+ const child = spawnImpl(executable, args, { env, stdio: "inherit" });
177
+ const forwardSigint = () => child.kill("SIGINT");
178
+ const forwardSigterm = () => child.kill("SIGTERM");
179
+ const removeSignalListeners = () => {
180
+ signalSource.removeListener("SIGINT", forwardSigint);
181
+ signalSource.removeListener("SIGTERM", forwardSigterm);
182
+ };
183
+ signalSource.on("SIGINT", forwardSigint);
184
+ signalSource.on("SIGTERM", forwardSigterm);
185
+ child.once("error", () => {
186
+ removeSignalListeners();
187
+ reject(new Error("无法启动 RainSkills 运行环境连接器"));
188
+ });
189
+ child.once("close", (code, signal) => {
190
+ removeSignalListeners();
191
+ resolve({ code, signal });
192
+ });
175
193
  });
176
194
  }
177
195
 
@@ -416,7 +434,7 @@ async function runBuiltin(args, {
416
434
  intent: null,
417
435
  operation_id: operationId,
418
436
  };
419
- manager.startConnecting(connection);
437
+ manager.startConnecting(connection, { replaceExisting: true });
420
438
  try {
421
439
  const invocation = runtimeConnectionInvocation(options, inspection.origin);
422
440
  let completedWithCredential = false;
@@ -774,6 +792,7 @@ module.exports = {
774
792
  classifyNodeMajor,
775
793
  parseRuntimeConnectArgs,
776
794
  resolveInvocation,
795
+ runAttached,
777
796
  runAutoUpdatePhase,
778
797
  runBuiltin,
779
798
  runtimeChildEnvironment,
package/install.sh CHANGED
@@ -1772,6 +1772,20 @@ write_device_token_request_body() {
1772
1772
  chmod 600 "$body_file"
1773
1773
  }
1774
1774
 
1775
+ assert_device_authorization_operation_current() {
1776
+ local base_url="$1"
1777
+ [[ "${RAINSKILLS_RUNTIME_CONNECT_COMPLETION:-0}" == "1" ]] || return 0
1778
+ valid_runtime_operation_id "${RAINSKILLS_RUNTIME_OPERATION_ID:-}" || return 1
1779
+
1780
+ local environment_kind="private"
1781
+ [[ "$DEPLOYMENT_MODE_INPUT" == "saas" ]] && environment_kind="saas"
1782
+ node "$SCRIPT_DIR/bin/rainskills.js" runtime assert-connect \
1783
+ --onboarding-id "$RAINSKILLS_RUNTIME_OPERATION_ID" \
1784
+ --target "$TARGET" \
1785
+ --environment-kind "$environment_kind" \
1786
+ --console-origin "$base_url" >/dev/null 2>&1
1787
+ }
1788
+
1775
1789
  poll_device_authorization() {
1776
1790
  local base_url="$1"
1777
1791
  local interval="$DEVICE_FLOW_INTERVAL"
@@ -1790,6 +1804,10 @@ poll_device_authorization() {
1790
1804
 
1791
1805
  while true; do
1792
1806
  device_flow_sleep "$interval"
1807
+ if ! assert_device_authorization_operation_current "$base_url"; then
1808
+ DEVICE_FLOW_ERROR="本次设备授权已被新的授权尝试替换。"
1809
+ return 1
1810
+ fi
1793
1811
  now="$(device_flow_now)"
1794
1812
  if [[ "$now" -ge "$deadline" ]]; then
1795
1813
  DEVICE_FLOW_ERROR="Rainbond 设备授权超时,请重新运行安装。"
@@ -1871,6 +1889,7 @@ device_flow_login_to_rainbond() {
1871
1889
  printf '请在任意能够访问该 Rainbond 平台的电脑上打开上面的地址并完成登录授权。\n' >&2
1872
1890
  fi
1873
1891
  printf '[RAINSKILLS_USER_MESSAGE_END:runtime.device-authorization]\n' >&2
1892
+ printf '[RAINSKILLS_AGENT_WAIT_REQUIRED:runtime-connect]\n' >&2
1874
1893
 
1875
1894
  if ! poll_device_authorization "$base_url"; then
1876
1895
  cleanup_device_flow
@@ -2101,6 +2120,8 @@ validate_mcp_connectivity() {
2101
2120
  curl \
2102
2121
  --silent \
2103
2122
  --show-error \
2123
+ --connect-timeout 10 \
2124
+ --max-time 30 \
2104
2125
  --output "$response_file" \
2105
2126
  --dump-header "$header_file" \
2106
2127
  --write-out '%{http_code}' \
@@ -2399,6 +2420,7 @@ obtain_rainbond_token() {
2399
2420
 
2400
2421
  local device_flow_status
2401
2422
  if device_flow_login_to_rainbond "$base_url"; then
2423
+ printf '浏览器授权已完成,正在验证 Rainbond 连接…\n' >&2
2402
2424
  return 0
2403
2425
  else
2404
2426
  device_flow_status=$?
@@ -2409,6 +2431,7 @@ obtain_rainbond_token() {
2409
2431
  die "当前 Rainbond Console 仅支持旧版浏览器回调;此模式需要本机浏览器或交互终端。请升级 Rainbond Console 后重试。"
2410
2432
  fi
2411
2433
  browser_login_to_rainbond "$base_url"
2434
+ printf '浏览器授权已完成,正在验证 Rainbond 连接…\n' >&2
2412
2435
  return 0
2413
2436
  fi
2414
2437
  die "${DEVICE_FLOW_ERROR:-Rainbond 设备授权失败。}"
@@ -2612,6 +2635,7 @@ main() {
2612
2635
  if [[ "${RAINSKILLS_RUNTIME_CONNECT_COMPLETION:-0}" == "1" ]]; then
2613
2636
  node "$SCRIPT_DIR/bin/rainskills.js" runtime complete-connect \
2614
2637
  --onboarding-id "$RAINSKILLS_RUNTIME_OPERATION_ID"
2638
+ printf '[RAINSKILLS_AGENT_WAIT_COMPLETE:runtime-connect]\n' >&2
2615
2639
  fi
2616
2640
  return 0
2617
2641
  fi
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Install the complete Rainskills AI deployment skill suite as one product.",
5
5
  "author": {
6
6
  "name": "Goodrain",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Install the complete Rainskills AI deployment skill suite as one product.",
5
5
  "author": {
6
6
  "name": "Goodrain",
@@ -14,7 +14,7 @@ This is the single marketplace entry for the complete Rainskills product. The in
14
14
  3. Keep stdin, stdout, and stderr attached. When `RAINSKILLS_USER_INPUT_REQUIRED` appears, pause for that installer choice. If the installer emits `rainskills.next-action.v1`, execute only its fixed `argv` through the same launcher; never evaluate output as a shell command. If the adjacent `bin/rainskills.js` exists, use it for fixed next actions; otherwise use the same versioned npm package fallback described below.
15
15
  4. Stay attached until every independent Skill is installed. Do not select, connect, or configure an application runtime during installation. In the user-facing response, output only the fixed completion message below.
16
16
 
17
- If the adjacent installer is missing, check the local Node.js version before choosing the fallback. With `npx` and Node.js 18 or newer, use `npx --yes rainskills@0.1.26 <target>`. With no Node.js or a version below 18, use `bash <(curl -fsSL https://get.rainbond.com/rainskills/install.sh) <target>` instead. Omit `<target>` only when the host cannot be determined reliably. Keep either command attached to the interactive terminal. For an update or repair, refresh this marketplace Skill first, then run the installer again; it compares and updates every independent internal Skill.
17
+ If the adjacent installer is missing, check the local Node.js version before choosing the fallback. With `npx` and Node.js 18 or newer, use `npx --yes rainskills@0.1.28 <target>`. With no Node.js or a version below 18, use `bash <(curl -fsSL https://get.rainbond.com/rainskills/install.sh) <target>` instead. Omit `<target>` only when the host cannot be determined reliably. Keep either command attached to the interactive terminal. For an update or repair, refresh this marketplace Skill first, then run the installer again; it compares and updates every independent internal Skill.
18
18
 
19
19
  Skills-only 安装不需要 Node.js;CDN fallback 只负责安装 Skill 文件,不代表运行环境连接、应用部署或平台安装已经可执行。用户首次提出需要运行环境的动作时,对应业务 Skill 才检查 Node.js;固定 Rainskills launcher 需要 Node.js 18 或更高版本。缺失或版本过低时保留原始 intent 并停止,等待用户或 agent 明确同意安装或升级 Node.js,安装完成消息不得提前提示 Node.js。
20
20
 
@@ -44,7 +44,11 @@ Rainskills 安装完成,下一条消息即可直接使用。
44
44
 
45
45
  `runtime connect` 的 Device Flow 不依赖 stdin TTY;Agent 必须执行固定 argv 并保持进程附着直到授权完成。能打开本机浏览器时由连接器自动跳转,SSH、容器等无浏览器场景原样展示授权地址并继续轮询。只有 Rainbond 不支持 Device Flow 且进入旧版 loopback 手动粘贴时才需要交互终端;不得要求用户在聊天中粘贴 JWT。
46
46
 
47
- Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.26`):
47
+ 授权命令是同步门禁。执行工具返回“进程仍在运行”或会话 ID 时,必须只等待或轮询同一个命令会话;在该会话结束前,禁止读取专项 Skill、解析 context、调用业务 CLI 或执行任何后续业务步骤。浏览器页面显示成功不代表连接完成;只有原命令退出码为 0,并输出 `rainskills.runtime-connect-result.v1` 且 `state=connected`,才可继续。不得另起 `runtime status` 猜测完成,也不得重复提示用户授权。
48
+
49
+ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id` 反复调用 `write_stdin`(空输入轮询),直到工具返回 `exit_code`。连接器输出 `[RAINSKILLS_AGENT_WAIT_REQUIRED:runtime-connect]` 后进入上述轮询;看到 `[RAINSKILLS_AGENT_WAIT_COMPLETE:runtime-connect]` 后仍须继续轮询,直到取得退出码和最终 JSON。
50
+
51
+ Rainskills 只保存一个全局运行环境,不维护环境列表、名称、默认值或环境 ID。使用固定 launcher `node <home>/.rainbond/lib/rainskills/bin/rainskills.js`(运行包版本 `rainskills@0.1.28`):
48
52
 
49
53
  - 状态:执行 `runtime status --json`。
50
54
  - 首次连接:执行 `runtime connect <target> --saas` 或 `runtime connect <target> --rainbond-url <Console origin>`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rainskills",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Interactive Rainbond skill installer for Codex, Claude Code, and Pi Agent",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/goodrain/rainskills#readme",
@@ -40,6 +40,7 @@
40
40
  "rainbond-template-installer/",
41
41
  "scripts/build-skill-manifest.mjs",
42
42
  "scripts/install-local-cli.mjs",
43
+ "scripts/validate_customer_output.py",
43
44
  "!**/__pycache__/",
44
45
  "!**/*.pyc"
45
46
  ],
@@ -67,7 +68,7 @@
67
68
  "test:launcher": "node --test tests/console-origin.test.js tests/local-cli-installer.test.js tests/npx-launcher.test.js tests/runtime-state.test.js tests/single-runtime.test.js tests/single-runtime-cli.test.js tests/single-runtime-launcher.test.js tests/skill-manifest-builder.test.js",
68
69
  "test:api-bridge": "node --test tests/api-bridge.test.js",
69
70
  "test:console-contract": "python3 tests/console_tool_contract_test.py",
70
- "test:skill-profile": "node --test tests/skill-profile-builder.test.js tests/markdown-link-integrity.test.js",
71
+ "test:skill-profile": "node --test tests/skill-profile-builder.test.js tests/markdown-link-integrity.test.js tests/user-facing-output-contract.test.js",
71
72
  "test:marketplace": "node --test tests/marketplace-entry.test.js",
72
73
  "test:runtime-routing": "node --test tests/runtime-onboarding-routing.test.js && python3 rainbond-app-assistant/scripts/validate_progressive_loading.py && python3 rainbond-app-assistant/scripts/validate_cross_skill_routing.py && python3 tests/progressive-loading-validators.test.py && python3 tests/run_skill_routing_evals.py",
73
74
  "test:telemetry": "node --test tests/telemetry.test.js",
@@ -27,7 +27,7 @@ description: "Use whenever a user asks to deploy, run, deliver, publish, inspect
27
27
  | 需要对象边界或跨阶段状态语义 | [product object model](references/product-object-model.md) | 不相关工作流 |
28
28
  | 需要生成最终结果或自动化契约 | [output contract](references/output-contract.md) | 不需要结果协议时不得读取 |
29
29
 
30
- workspace context 包含 `enterprise_id`、`team_id`、`team_name` 和 `region_name`;app/component 标识只来自用户明确输入或本次实时查询/创建结果。它们都由当前任务携带,不写入 Runtime,也不得生成或传递 CLI 业务 operation ID、运行环境 ID 或 intent JSON。
30
+ workspace context 包含 `enterprise_id`、`team_id`、`team_name` 和 `region_name`;`team_id` 可作为内部调用参数继续使用,但不得复制到用户可见的过程消息或最终结果中。app/component 标识只来自用户明确输入或本次实时查询/创建结果。它们都由当前任务携带,不写入 Runtime,也不得生成或传递 CLI 业务 operation ID、运行环境 ID 或 intent JSON。
31
31
 
32
32
  ## Runtime Gate
33
33
 
@@ -57,6 +57,8 @@ workspace context 包含 `enterprise_id`、`team_id`、`team_name` 和 `region_n
57
57
 
58
58
  - 成功:说明项目、运行环境、工作空间、应用、真实 Rainbond 页面/访问地址及本轮实际完成操作;无法确认的字段省略,禁止猜测。
59
59
  - 未完成:输出“部署失败。”、一句直接原因;仅在确有安全可执行方案时增加“解决办法”。
60
+ - 默认不得展示内部对象、状态枚举、Skill/工具名、YAML、JSON 或英文编排标题。
61
+ - 过程消息和最终结果中的工作空间一律用 `team_name` 展示,可连同 `region_name` 写成“工作空间 / 集群”;除非用户明确要求调试原始数据,不展示 `team_id`。
60
62
  - 用户明确要求结构化结果或自动化契约时,读取 [output contract](references/output-contract.md),不得自行发明字段、枚举或状态。
61
63
 
62
64
  ## 停止条件
@@ -1,4 +1,5 @@
1
1
  case: linked-topology-missing
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.request_intent: source_app_delivery
@@ -1,4 +1,5 @@
1
1
  case: linked-topology-building
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.request_intent: source_app_delivery
@@ -1,4 +1,5 @@
1
1
  case: runtime-healthy-delivery-unknown
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.request_intent: source_app_delivery
@@ -1,5 +1,5 @@
1
1
  case: delivered-stop-without-promotion
2
- presentation_mode: concise
2
+ presentation_mode: customer
3
3
  assert:
4
4
  prose_contains:
5
5
  - 部署成功
@@ -1,4 +1,5 @@
1
1
  case: delivered-explicit-dev-to-test
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.request_intent: dev_to_test_promotion
@@ -1,4 +1,5 @@
1
1
  case: manual-validation-stop
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.request_intent: dev_to_test_promotion
@@ -1,4 +1,5 @@
1
1
  case: cluster-capacity-blocked
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.orchestration_state: linked-and-cluster-capacity-blocked
@@ -1,4 +1,5 @@
1
1
  case: code-build-handoff-stop
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.orchestration_state: linked-and-needs-code-build-handoff
@@ -1,4 +1,5 @@
1
1
  case: source-backed-failure-no-fallback
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.execution_path.requested_kind: source
@@ -1,4 +1,5 @@
1
1
  case: identity-ambiguous-stop
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.orchestration_state: linked-and-identity-ambiguous
@@ -1,5 +1,5 @@
1
1
  case: delivered-with-proxy-and-local-binding
2
- presentation_mode: concise
2
+ presentation_mode: customer
3
3
  assert:
4
4
  prose_contains:
5
5
  - 部署成功
@@ -1,4 +1,5 @@
1
1
  case: complex-suite-needs-descriptor-stop
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.request_intent: source_app_delivery
@@ -1,4 +1,5 @@
1
1
  case: explicit-git-url-locks-source
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.request_intent: source_app_delivery
@@ -1,4 +1,5 @@
1
1
  case: strategy-switch-cleans-residue
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.request_intent: source_app_delivery
@@ -1,4 +1,5 @@
1
1
  case: turn-discipline-reuse-alias
2
+ presentation_mode: structured
2
3
  assert:
3
4
  equal:
4
5
  AppAssistantResult.request_intent: source_app_delivery
@@ -1,5 +1,5 @@
1
1
  case: source-delivered-manual-validation
2
- presentation_mode: concise
2
+ presentation_mode: customer
3
3
  assert:
4
4
  prose_contains:
5
5
  - 部署成功,待浏览器访问确认
@@ -13,7 +13,7 @@ Load this reference only when checking a proposed route or reviewing a completed
13
13
  - reporting team/region/app identity as a substitute for the clickable Rainbond deployment location
14
14
  - using the public service URL as the Rainbond deployment location, or constructing the public service URL from naming conventions
15
15
  - stripping useful diagnostic evidence from building, blocked, unhealthy, ambiguous, handoff, or incomplete promotion states
16
- - omitting the required `### Structured Output` section in structured contract mode
16
+ - omitting `### Structured Output` after explicit structured contract mode was selected
17
17
  - replacing the required five human-readable sections with freeform narrative in structured contract mode
18
18
  - treating a project as unlinked only because `.rainbond/local.json.metadata.status` is stale even though the platform confirms the same app in the current run
19
19
  - omitting `Actions Performed` detail when the run only did inspection/classification and intentionally skipped downstream skills
@@ -30,22 +30,11 @@
30
30
  - concise delivery report mode
31
31
  - structured contract mode
32
32
 
33
- ### Concise delivery report mode
34
-
35
- Use this mode by default when all of the following are true:
36
- - `request_intent = source_app_delivery`
37
- - `runtime_state.phase = runtime_healthy`
38
- - `delivery_state.status` is `delivered` or `delivered-but-needs-manual-validation`
39
- - `delivery_state.verification_mode` is `verified`, `inferred`, or `manual_validation_needed` consistently with that status
40
- - `next_action` is `stop` or `stop and validate URL manually`
41
- - `promotion_result = null`
42
- - there is no unresolved `runtime_state.blocker` or `delivery_state.blocker`
43
- - `project.deployment_location_url` is non-null
44
- - `delivery_state.preferred_access_url` is non-null
45
- - the user did not explicitly request structured output, YAML, JSON, debug output, or machine-readable output
46
- - no eval/automation consumer explicitly requires structured contract mode
47
-
48
- In concise delivery report mode:
33
+ ### Customer report mode
34
+
35
+ Use this mode by default for successful, incomplete, blocked, ambiguous, and handoff results. Switch away from it only when the user explicitly requests structured output, YAML, JSON, debug details, or machine-readable output, or when an automation/evaluation consumer explicitly declares the structured contract requirement.
36
+
37
+ In customer report mode:
49
38
  - do not append `### Structured Output`
50
39
  - do not expose the fenced YAML block
51
40
  - keep the report short and directly useful to the user
@@ -93,17 +82,13 @@
93
82
  Use this mode when any of the following is true:
94
83
  - the user asks for structured output, YAML, JSON, debug details, or machine-readable output
95
84
  - an eval, wrapper, or automation flow explicitly needs deterministic structured schema validation
96
- - any concise delivery report condition above is not met
97
- - the app is building, unhealthy, blocked, identity-ambiguous, or requires handoff
98
- - `promotion_result` is non-null or the user requested dev-to-test promotion
99
- - there is any unresolved blocker or handoff
100
85
  - another skill or wrapper will consume the result as input
101
86
 
102
- Building, unhealthy, blocked, ambiguous, handoff, and incomplete promotion states should keep the detailed human-readable sections and evidence below. Do not make non-success output terse merely because successful output is concise.
87
+ Building, unhealthy, blocked, ambiguous, handoff, and incomplete promotion states do not enable structured mode by themselves. In customer mode, explain the direct result and one actionable next step in concise Chinese without exposing internal enums or action ledgers.
103
88
 
104
89
  In structured contract mode:
105
90
  - the human-readable sections below are the narrative view over `AppAssistantResult`
106
- - the reply must end with a final `### Structured Output` section
91
+ - the explicitly structured reply ends with a final `### Structured Output` section
107
92
  - the `### Structured Output` section must render `AppAssistantResult` in fenced `yaml`
108
93
  - the literal section order must be:
109
94
  - `### Project State`
@@ -394,7 +379,7 @@
394
379
  ```
395
380
  ````
396
381
 
397
- In structured contract mode, always respond using exactly these sections:
382
+ Only in explicit structured contract mode, respond using exactly these sections:
398
383
 
399
384
  ### Project State
400
385
  - state the current classification
@@ -9,14 +9,18 @@ Canonical progressive-loading contract: `rainskills.skill-runtime-contract.v1`.
9
9
 
10
10
  没有运行环境时,让用户选择 Rainbond Cloud 或一个已有/新建的私有 Rainbond,并执行对应的 `runtime connect`。连接和重新授权必须进入浏览器 Device Flow,不复用 Shell 中缓存的 JWT;新凭据通过 live probe 后才覆盖唯一运行环境。CLI 返回 401 时,只读调用可在 `runtime reconnect` 成功后重试一次;写调用不得自动重放,必须先查询平台真实状态。403 直接停止,不重新授权。
11
11
 
12
- `context resolve` 是无状态调用:单一工作空间直接返回上下文,多个候选返回组合选项;用户选择后由当前任务直接携带 team/region 参数,不执行 `context select`,不写本地 operation。所有可变 `call` 仍需先取得 confirmation ID,再以完全相同的输入追加 `--confirm` 执行一次。
12
+ 授权命令是同步门禁。执行工具返回“进程仍在运行”或会话 ID 时,必须只等待或轮询同一个命令会话;在该会话结束前,禁止读取专项 Skill、解析 context、调用业务 CLI 或执行任何后续业务步骤。浏览器页面显示成功不代表连接完成;只有原命令退出码为 0,并输出 `rainskills.runtime-connect-result.v1` `state=connected`,才可继续。不得另起 `runtime status` 猜测完成,也不得重复提示用户授权。
13
+
14
+ Codex 中命令工具一旦返回 `session_id`,必须立即对该 `session_id` 反复调用 `write_stdin`(空输入轮询),直到工具返回 `exit_code`。连接器输出 `[RAINSKILLS_AGENT_WAIT_REQUIRED:runtime-connect]` 后进入上述轮询;看到 `[RAINSKILLS_AGENT_WAIT_COMPLETE:runtime-connect]` 后仍须继续轮询,直到取得退出码和最终 JSON。
15
+
16
+ `context resolve` 是无状态调用:单一工作空间直接返回上下文,多个候选返回组合选项;其中 `team_id` 是后续平台调用的内部参数,不得复制到用户可见的过程消息或最终结果中。用户选择后由当前任务直接携带 team/region 参数,不执行 `context select`,不写本地 operation。所有可变 `call` 仍需先取得 confirmation ID,再以完全相同的输入追加 `--confirm` 执行一次。
13
17
 
14
18
  `required` 只声明要解析的维度,企业 ID 始终来自当前登录身份。用户明确给出的 team/region 必须放进 `hints` 做精确匹配;不得把企业名、team 名或选择对象作为顶层 `enterprise` / `workspace` 字段传入。多候选时只展示 CLI 返回的 label;用户选择后再次执行同一个无状态 `context resolve`,通过 `selection.option_id` 让 CLI 重新查询并验证当前候选,不写本地 context 状态。
15
19
 
16
20
  ```json
17
21
  {
18
22
  "schema": "rainskills.single-runtime-contract.v1",
19
- "package_version": "rainskills@0.1.26",
23
+ "package_version": "rainskills@0.1.28",
20
24
  "runtime_status": [
21
25
  "node",
22
26
  "<home>/.rainbond/lib/rainskills/bin/rainskills.js",
@@ -529,7 +529,7 @@
529
529
  反例(来自真实回归 case `cs_1779149681822_3u`,2026-05-19):模型从 `rainbond_get_component_summary` 响应的 `recent_events` 段看到形如 `{"ID": 17740, "event_id": "...", "opt_type": "build-service"}`,直接把 `17740` 当成 `event_id` 传给 `rainbond_get_component_build_logs`,工具返回 `items: []`(找不到该 UUID 的日志)。正确做法:从同一 `recent_events[i].event_id` 字段取出 UUID 字符串,或调 `rainbond_get_component_events` 重查。
530
530
  35. **会话内部叙述纪律 + 内部 preflight 工具不要主动调**。下列四类是"内部会话状态",对用户**无信息量**,禁止外漏到 assistant 可见消息:
531
531
  - **专项 Skill 手册加载过程**:这是当前任务的内部准备。不要说"我先加载 bootstrap 手册"、"手册已加载"或"skill ready";读完直接进入下一个真实工具调用。
532
- - **`rainbond_get_current_user`**:不得由 Agent 单独调用。`enterprise_id`、`team_id`、`team_name` 和 `region_name` 统一由受保护的 `context resolve` 命令无状态解析并由当前任务携带;只有 CLI 明确返回需要选择时才询问用户。
532
+ - **`rainbond_get_current_user`**:不得由 Agent 单独调用。`enterprise_id`、`team_id`、`team_name` 和 `region_name` 统一由受保护的 `context resolve` 命令无状态解析并由当前任务携带;`team_id` 可继续用于后续平台调用,但不得在过程消息或最终报告中展示。只有 CLI 明确返回需要选择时才询问用户,并且只展示名称标签。
533
533
  - **`rainbond_query_components` 同入参重复轮询**:服务端 30s 内的同 args 调用会走缓存;你**不要**在每个新 user turn 开头都"先查一下组件列表"。只有本次调用使用的 `enterprise_id`、`app_id` 和查询条件均未变化,且没有新的平台动作时,才可复用 priorTurnMessages 里的最近结果。
534
534
  - **规则推理过程 / Rainbond Tool 内部限制 / 分类决策叙述**:你内部基于哪条 Iron Law / hard rule / 推断信号做的决策、具体 Tool 签名 / 字段限制、对组件 / 服务的分类判断("ClickHouse 是公认的列式分析数据库"这类),都属于内部状态,对用户**无信息量**。
535
535