rainskills 0.1.0 → 0.1.2

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 (116) hide show
  1. package/README.md +156 -420
  2. package/SKILL.md +37 -7
  3. package/agents/openai.yaml +2 -2
  4. package/bin/rainskills-tools.js +712 -0
  5. package/bin/rainskills.js +1109 -4
  6. package/install.sh +536 -63
  7. package/marketplace/rainskills/.claude-plugin/plugin.json +2 -2
  8. package/marketplace/rainskills/.codex-plugin/plugin.json +5 -5
  9. package/marketplace/rainskills/skills/rainskills/SKILL.md +38 -8
  10. package/package.json +14 -5
  11. package/pi/rainskills-mcp.ts +214 -1850
  12. package/rainbond-app-assistant/SKILL.md +123 -49
  13. package/rainbond-app-assistant/evals/01-linked-topology-missing.response.md +3 -3
  14. package/rainbond-app-assistant/evals/02-linked-topology-building.response.md +3 -3
  15. package/rainbond-app-assistant/evals/03-runtime-healthy-delivery-unknown.response.md +3 -3
  16. package/rainbond-app-assistant/evals/04-delivered-stop-without-promotion.expected.yaml +1 -1
  17. package/rainbond-app-assistant/evals/04-delivered-stop-without-promotion.response.md +1 -1
  18. package/rainbond-app-assistant/evals/05-delivered-explicit-dev-to-test.response.md +3 -3
  19. package/rainbond-app-assistant/evals/06-manual-validation-stop.expected.yaml +1 -1
  20. package/rainbond-app-assistant/evals/06-manual-validation-stop.response.md +3 -3
  21. package/rainbond-app-assistant/evals/07-cluster-capacity-blocked.response.md +4 -4
  22. package/rainbond-app-assistant/evals/08-code-build-handoff-stop.response.md +2 -2
  23. package/rainbond-app-assistant/evals/09-source-backed-failure-no-fallback.response.md +2 -2
  24. package/rainbond-app-assistant/evals/10-identity-ambiguous-stop.response.md +3 -3
  25. package/rainbond-app-assistant/evals/12-complex-suite-needs-descriptor-stop.response.md +2 -2
  26. package/rainbond-app-assistant/evals/13-explicit-git-url-locks-source.response.md +2 -2
  27. package/rainbond-app-assistant/evals/14-strategy-switch-cleans-residue.response.md +2 -2
  28. package/rainbond-app-assistant/evals/15-turn-discipline-reuse-alias.response.md +2 -2
  29. package/rainbond-app-assistant/references/operational-reference.md +77 -0
  30. package/rainbond-app-assistant/references/output-contract.md +454 -0
  31. package/rainbond-app-assistant/references/product-object-model.md +1558 -0
  32. package/rainbond-app-assistant/references/workflow-rules.md +343 -0
  33. package/rainbond-app-assistant/schemas/app-assistant-result.schema.yaml +6 -1
  34. package/rainbond-app-version-assistant/SKILL.md +57 -18
  35. package/rainbond-delivery-verifier/SKILL.md +86 -13
  36. package/rainbond-env-sync/SKILL.md +52 -11
  37. package/rainbond-fullstack-bootstrap/SKILL.md +79 -11
  38. package/rainbond-fullstack-bootstrap/evals/01-reuse-only.response.md +1 -1
  39. package/rainbond-fullstack-bootstrap/evals/02-deferred-dependency.response.md +1 -1
  40. package/rainbond-fullstack-bootstrap/evals/03-capacity-blocked.response.md +1 -1
  41. package/rainbond-fullstack-bootstrap/evals/04-template-skip.response.md +1 -1
  42. package/rainbond-fullstack-bootstrap/evals/05-source-ambiguity-stop.response.md +1 -1
  43. package/rainbond-fullstack-bootstrap/evals/06-external-artifact-unreachable.response.md +1 -1
  44. package/rainbond-fullstack-bootstrap/evals/07-provider-connection-contract.response.md +1 -1
  45. package/rainbond-fullstack-bootstrap/evals/08-explicit-dependency-after-manual-components.response.md +1 -1
  46. package/rainbond-fullstack-bootstrap/evals/09-inferred-provider-consumer-gate.response.md +1 -1
  47. package/rainbond-fullstack-bootstrap/evals/10-compose-service-name-not-hostname.response.md +1 -1
  48. package/rainbond-fullstack-bootstrap/evals/11-reverse-proxy-not-dropped.response.md +1 -1
  49. package/rainbond-fullstack-bootstrap/evals/12-optional-services-disclose-not-deploy.response.md +1 -1
  50. package/rainbond-fullstack-bootstrap/evals/13-env-reference-dependency-completeness.response.md +1 -1
  51. package/rainbond-fullstack-bootstrap/evals/14-client-package-upload.expected.yaml +1 -1
  52. package/rainbond-fullstack-bootstrap/evals/14-client-package-upload.response.md +3 -3
  53. package/rainbond-fullstack-bootstrap/evals/15-unfinished-dockerfile-recovery.expected.yaml +25 -0
  54. package/rainbond-fullstack-bootstrap/evals/15-unfinished-dockerfile-recovery.response.md +44 -0
  55. package/rainbond-fullstack-bootstrap/evals/16-complete-cnb-recreate-confirmed.expected.yaml +25 -0
  56. package/rainbond-fullstack-bootstrap/evals/16-complete-cnb-recreate-confirmed.response.md +44 -0
  57. package/rainbond-fullstack-bootstrap/modules/10-context-loading.md +8 -8
  58. package/rainbond-fullstack-bootstrap/modules/30-creation-rules.md +10 -10
  59. package/rainbond-fullstack-bootstrap/modules/40-source-and-package-rules.md +13 -9
  60. package/rainbond-fullstack-bootstrap/modules/50-workflow-and-convergence.md +6 -6
  61. package/rainbond-fullstack-bootstrap/modules/60-verification-and-handoffs.md +4 -4
  62. package/rainbond-fullstack-bootstrap/modules/70-output-contract.md +3 -3
  63. package/rainbond-fullstack-bootstrap/references/quick-reference.md +6 -6
  64. package/rainbond-fullstack-bootstrap/references/source-build-parameter-guide.md +6 -6
  65. package/rainbond-fullstack-bootstrap/references/sql-init-recipe.md +3 -3
  66. package/rainbond-fullstack-bootstrap/schemas/bootstrap-result.schema.yaml +3 -2
  67. package/rainbond-fullstack-bootstrap/scripts/upload_local_package.py +1 -1
  68. package/rainbond-fullstack-bootstrap/scripts/validate_bootstrap_output.py +2 -2
  69. package/rainbond-fullstack-troubleshooter/SKILL.md +103 -13
  70. package/rainbond-fullstack-troubleshooter/evals/10-config-file-configmap-missing.expected.yaml +21 -0
  71. package/rainbond-fullstack-troubleshooter/evals/10-config-file-configmap-missing.response.md +51 -0
  72. package/rainbond-fullstack-troubleshooter/references/operational-reference.md +70 -0
  73. package/rainbond-fullstack-troubleshooter/references/output-contract.md +259 -0
  74. package/rainbond-fullstack-troubleshooter/references/root-cause-rules.md +210 -0
  75. package/rainbond-fullstack-troubleshooter/schemas/troubleshoot-result.schema.yaml +3 -1
  76. package/rainbond-fullstack-troubleshooter/scripts/validate_troubleshoot_output.py +22 -1
  77. package/rainbond-platform-installer/SKILL.md +73 -9
  78. package/rainbond-platform-installer/agents/openai.yaml +2 -2
  79. package/rainbond-platform-installer/references/installation-policy.json +35 -1
  80. package/rainbond-platform-installer/references/installation-policy.md +33 -8
  81. package/rainbond-platform-installer/references/troubleshooting.md +3 -2
  82. package/rainbond-platform-installer/scripts/auto-update.js +495 -0
  83. package/rainbond-platform-installer/scripts/console-origin.js +159 -0
  84. package/rainbond-platform-installer/scripts/environment-credentials.js +225 -0
  85. package/rainbond-platform-installer/scripts/environment-registry.js +349 -0
  86. package/rainbond-platform-installer/scripts/existing-kubernetes-installer.js +1370 -0
  87. package/rainbond-platform-installer/scripts/host-cluster-installer.js +2143 -0
  88. package/rainbond-platform-installer/scripts/local-runtime-commands.js +180 -0
  89. package/rainbond-platform-installer/scripts/local-runtime.js +18 -0
  90. package/rainbond-platform-installer/scripts/mcp-router.js +212 -0
  91. package/rainbond-platform-installer/scripts/mcp-server.js +79 -0
  92. package/rainbond-platform-installer/scripts/platform-installer.js +694 -255
  93. package/rainbond-platform-installer/scripts/platform-routing.js +326 -0
  94. package/rainbond-platform-installer/scripts/runtime-credentials.js +163 -0
  95. package/rainbond-platform-installer/scripts/runtime-intents.js +251 -0
  96. package/rainbond-platform-installer/scripts/runtime-operations.js +383 -0
  97. package/rainbond-platform-installer/scripts/runtime-state.js +656 -0
  98. package/rainbond-platform-installer/scripts/secure-state.js +59 -13
  99. package/rainbond-platform-installer/scripts/ssh-key-setup.js +242 -0
  100. package/rainbond-platform-installer/scripts/telemetry.js +244 -0
  101. package/rainbond-platform-installer/scripts/user-message.js +125 -0
  102. package/rainbond-platform-installer/scripts/windows-auth.js +1 -1
  103. package/rainbond-platform-installer/scripts/windows-client-config.js +101 -22
  104. package/rainbond-platform-installer/scripts/windows-onboarding.js +179 -83
  105. package/rainbond-platform-installer/scripts/windows-platform.js +3 -3
  106. package/rainbond-platform-installer/scripts/windows-platform.ps1 +264 -28
  107. package/rainbond-platform-installer/scripts/windows-read-user-environment.ps1 +16 -0
  108. package/rainbond-platform-query/SKILL.md +88 -0
  109. package/rainbond-platform-query/agents/openai.yaml +4 -0
  110. package/rainbond-platform-query/evals/01-current-enterprise.expected.yaml +10 -0
  111. package/rainbond-project-init/SKILL.md +68 -9
  112. package/rainbond-project-init/references/manifest-rules.md +487 -0
  113. package/rainbond-project-init/references/operational-reference.md +74 -0
  114. package/rainbond-project-init/references/output-contract.md +272 -0
  115. package/rainbond-project-init/references/workflow-and-verification.md +85 -0
  116. package/rainbond-template-installer/SKILL.md +80 -16
package/install.sh CHANGED
@@ -9,12 +9,21 @@
9
9
  # hand-write ~/.rainbond/mcp.env. See README.md "给 AI 助手的指引".
10
10
  set -euo pipefail
11
11
 
12
+ RAINSKILLS_VERBOSE="${RAINSKILLS_VERBOSE:-0}"
13
+ for rainskills_bootstrap_arg in "$@"; do
14
+ if [[ "$rainskills_bootstrap_arg" == "--verbose" ]]; then
15
+ RAINSKILLS_VERBOSE=1
16
+ break
17
+ fi
18
+ done
19
+
12
20
  RAINBOND_SKILLS_HOME_DEFAULT="${RAINBOND_SKILLS_HOME:-$HOME/.rainbond/skills}"
13
21
  RAINBOND_SKILLS_OSS_TARBALL_URL="${RAINBOND_SKILLS_OSS_URL:-https://get.rainbond.com/rainskills/rainskills-latest.tar.gz}"
14
22
  RAINBOND_SKILLS_GITHUB_TARBALL_URL="https://github.com/goodrain/rainskills/archive/refs/heads/main.tar.gz"
15
23
  RAINBOND_SKILLS_TARBALL_URL_OVERRIDE="${RAINBOND_SKILLS_TARBALL_URL:-}"
16
24
 
17
25
  bootstrap_log() {
26
+ [[ "$RAINSKILLS_VERBOSE" == "1" ]] || return 0
18
27
  printf '%s\n' "$1"
19
28
  }
20
29
 
@@ -92,8 +101,10 @@ bootstrap_download_if_needed() {
92
101
  chmod +x "$target_script" 2>/dev/null || true
93
102
 
94
103
  bootstrap_log "切换到 $target_script 继续执行……"
95
- if [[ -r /dev/tty ]]; then
96
- exec bash "$target_script" "$@" </dev/tty
104
+ local controlling_tty=""
105
+ controlling_tty="$(tty 2>/dev/null || true)"
106
+ if [[ "$controlling_tty" == /dev/* && -r "$controlling_tty" ]]; then
107
+ exec bash "$target_script" "$@" <"$controlling_tty"
97
108
  else
98
109
  exec bash "$target_script" "$@"
99
110
  fi
@@ -114,8 +125,19 @@ should_skip_bootstrap_for_refresh() {
114
125
  return 1
115
126
  }
116
127
 
117
- if should_skip_bootstrap_for_refresh "$@"; then
118
- SCRIPT_DIR=""
128
+ is_runtime_connect_invocation() {
129
+ [[ "${1:-}" == "connect" ]]
130
+ }
131
+
132
+ if is_runtime_connect_invocation "$@"; then
133
+ [[ "${RAINSKILLS_RUNTIME_CONNECT_COMPLETION:-0}" == "1" ]] \
134
+ && [[ "${RAINSKILLS_RUNTIME_OPERATION_ID:-}" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89aAbB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ ]] \
135
+ || bootstrap_die "runtime connect 必须通过 Rainskills Node 入口执行。"
136
+ SCRIPT_DIR="$(resolve_script_dir)"
137
+ [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/bin/rainskills.js" ]] \
138
+ || bootstrap_die "runtime connect 缺少受信任的 Rainskills Node 入口。"
139
+ elif should_skip_bootstrap_for_refresh "$@"; then
140
+ SCRIPT_DIR="$(resolve_script_dir)"
119
141
  else
120
142
  bootstrap_download_if_needed "$@"
121
143
  fi
@@ -153,13 +175,15 @@ ACTIVE_SHELL_RC=""
153
175
  VALIDATED_TOKEN=""
154
176
  OBTAINED_RAINBOND_TOKEN=""
155
177
  RAINSKILLS_INSTALL_REPORT_URL="https://log.rainbond.com/api/rainskills/installations"
156
- RAINSKILLS_INSTALL_ATTEMPT_ID=""
178
+ RAINSKILLS_LIFECYCLE_REPORT_URL="https://log.rainbond.com/api/rainskills/lifecycle-events"
179
+ RAINSKILLS_INSTALL_ATTEMPT_ID="${RAINSKILLS_INSTALL_ATTEMPT_ID:-}"
157
180
  RAINSKILLS_INSTALL_EID=""
158
181
  RAINSKILLS_INSTALL_CLIENT="unknown"
159
182
  RAINSKILLS_INSTALL_ACTION="install"
160
183
  RAINSKILLS_INSTALL_FAILURE_STAGE="bootstrap"
161
184
  RAINSKILLS_INSTALL_FAILURE_CATEGORY="invalid_arguments"
162
185
  RAINSKILLS_INSTALL_TERMINAL_REPORTED=0
186
+ RAINSKILLS_TELEMETRY_SEQUENCE=0
163
187
  RAINSKILLS_BROWSER_LOGIN_SERVER_PID=""
164
188
  RAINSKILLS_BROWSER_LOGIN_READER_PID=""
165
189
  RAINSKILLS_BROWSER_LOGIN_RESULT_FILE=""
@@ -197,6 +221,7 @@ Options:
197
221
  remember to restart Claude Code / Codex afterwards)
198
222
  --dest PATH Install skills to a custom directory only
199
223
  --force Overwrite existing installed skills
224
+ --verbose Show installation paths and copy diagnostics
200
225
  --skip-mcp Skip Rainbond MCP setup
201
226
  --saas Use Rainbond Cloud (https://run.rainbond.com)
202
227
  --self-hosted Use a self-hosted Rainbond Console (requires --rainbond-url)
@@ -223,10 +248,34 @@ log() {
223
248
  printf '%s\n' "$1"
224
249
  }
225
250
 
251
+ install_detail_log() {
252
+ [[ "$RAINSKILLS_VERBOSE" == "1" ]] || return 0
253
+ log "$1"
254
+ }
255
+
226
256
  warn() {
227
257
  printf '警告:%s\n' "$1" >&2
228
258
  }
229
259
 
260
+ print_capability_summary() {
261
+ printf '[RAINSKILLS_USER_MESSAGE_BEGIN:install.completed]\n'
262
+ cat <<'EOF'
263
+ Rainskills 安装完成,下一条消息即可直接使用。
264
+
265
+ 下一步可以直接说:
266
+
267
+ - 帮我部署当前项目
268
+ - 帮我部署一个 Git 仓库
269
+ - 帮我通过镜像或安装包部署应用
270
+ - 帮我安装一个应用模板
271
+ - 帮我分析当前项目应该如何部署
272
+
273
+ 也可以直接告诉我你想部署什么应用。
274
+ EOF
275
+ printf '[RAINSKILLS_USER_MESSAGE_END:install.completed]\n'
276
+ printf '[RAINSKILLS_AGENT_SUMMARY_REQUIRED:include-next-actions]\n'
277
+ }
278
+
230
279
  new_rainskills_install_attempt_id() {
231
280
  if command -v python3 >/dev/null 2>&1; then
232
281
  python3 - <<'PY'
@@ -311,20 +360,169 @@ PY
311
360
  ) &
312
361
  }
313
362
 
363
+ rainskills_telemetry_target() {
364
+ if [[ -n "${RAINSKILLS_TELEMETRY_TARGET:-}" ]]; then
365
+ printf '%s\n' "$RAINSKILLS_TELEMETRY_TARGET"
366
+ elif [[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]]; then
367
+ printf 'local-macos\n'
368
+ else
369
+ printf 'local-linux\n'
370
+ fi
371
+ }
372
+
373
+ rainskills_telemetry_platform() {
374
+ case "$(uname -s 2>/dev/null || true)" in
375
+ Darwin) printf 'darwin\n' ;;
376
+ *) printf 'linux\n' ;;
377
+ esac
378
+ }
379
+
380
+ rainskills_telemetry_failure_phase() {
381
+ case "${RAINSKILLS_TELEMETRY_ERROR_STAGE:-bootstrap}" in
382
+ skill_installation|bootstrap) printf 'bootstrap\n' ;;
383
+ download) printf 'rootfs_download\n' ;;
384
+ authorization) printf 'authorize_legacy\n' ;;
385
+ verification) printf 'verify_console\n' ;;
386
+ configuration) printf 'configure_mcp\n' ;;
387
+ *) printf 'bootstrap\n' ;;
388
+ esac
389
+ }
390
+
391
+ rainskills_telemetry_error_code() {
392
+ case "${RAINSKILLS_INSTALL_FAILURE_CATEGORY:-unknown}" in
393
+ invalid_arguments) printf 'invalid_arguments\n' ;;
394
+ tarball_unavailable|download_failed) printf 'download_failed\n' ;;
395
+ authorization_failed) printf 'authorization_failed\n' ;;
396
+ mcp_verification_failed) printf 'mcp_verification_failed\n' ;;
397
+ mcp_configuration_failed|skill_installation_failed) printf 'configuration_failed\n' ;;
398
+ network_unreachable) printf 'network_unreachable\n' ;;
399
+ *) printf 'unknown\n' ;;
400
+ esac
401
+ }
402
+
403
+ report_rainskills_lifecycle_event() {
404
+ local lifecycle_phase="$1"
405
+ local step="$2"
406
+ local lifecycle_action="$3"
407
+ local lifecycle_status="$4"
408
+ local error_code="${5:-}"
409
+ local blocked_reason="${6:-}"
410
+ local auth_method="${7:-}"
411
+
412
+ [[ -n "$RAINSKILLS_INSTALL_ATTEMPT_ID" ]] || return 0
413
+ command -v python3 >/dev/null 2>&1 || return 0
414
+ RAINSKILLS_TELEMETRY_SEQUENCE=$((RAINSKILLS_TELEMETRY_SEQUENCE + 1))
415
+
416
+ local payload telemetry_dir
417
+ telemetry_dir="${RAINSKILLS_TELEMETRY_DIR:-${HOME:-/tmp}/.rainbond/rainskills/telemetry}"
418
+ payload="$({
419
+ python3 - \
420
+ "$RAINSKILLS_INSTALL_ATTEMPT_ID" \
421
+ "${RAINSKILLS_TELEMETRY_OPERATION_ID:-}" \
422
+ "${RAINSKILLS_TELEMETRY_INSTALLATION_ID:-}" \
423
+ "${RAINSKILLS_TELEMETRY_CLIENT:-$RAINSKILLS_INSTALL_CLIENT}" \
424
+ "${RAINSKILLS_TELEMETRY_CONTROL_MODE:-posix}" \
425
+ "$(rainskills_telemetry_target)" \
426
+ "$(rainskills_telemetry_platform)" \
427
+ "$RAINSKILLS_INSTALL_ACTION" \
428
+ "$RAINSKILLS_TELEMETRY_SEQUENCE" \
429
+ "$lifecycle_phase" \
430
+ "$step" \
431
+ "$lifecycle_action" \
432
+ "$lifecycle_status" \
433
+ "$error_code" \
434
+ "$blocked_reason" \
435
+ "$auth_method" \
436
+ "$RAINSKILLS_INSTALL_EID" \
437
+ "$RAINSKILLS_LIFECYCLE_REPORT_URL" \
438
+ "$telemetry_dir" <<'PY'
439
+ import datetime
440
+ import json
441
+ import os
442
+ import sys
443
+ import uuid
444
+
445
+ (attempt, operation, installation, client, control_mode, target, platform, action,
446
+ sequence, phase, step, lifecycle_action, status, error_code, blocked_reason,
447
+ auth_method, eid, report_url, telemetry_dir) = sys.argv[1:]
448
+ event = {
449
+ "schema": "rainskills.lifecycle-event.v1",
450
+ "event_id": str(uuid.uuid4()),
451
+ "install_attempt_id": attempt,
452
+ "operation_id": operation or None,
453
+ "installation_id": installation or None,
454
+ "parent_event_id": None,
455
+ "sequence": int(sequence),
456
+ "attempt": 1,
457
+ "resumed_from": None,
458
+ "package_version": os.environ.get("RAINSKILLS_PACKAGE_VERSION") or None,
459
+ "platform": platform,
460
+ "control_mode": control_mode,
461
+ "target": target,
462
+ "client": client if client in {"codex", "claude_code", "both", "unknown"} else "unknown",
463
+ "eid": eid or None,
464
+ "phase": None,
465
+ "lifecycle_phase": phase,
466
+ "step": step,
467
+ "action": action if action in {"install", "refresh"} else "install",
468
+ "lifecycle_action": lifecycle_action or None,
469
+ "status": None,
470
+ "lifecycle_status": status,
471
+ "duration_ms": None,
472
+ "error_code": error_code or None,
473
+ "error_stage": phase if error_code else None,
474
+ "reason_code": error_code or None,
475
+ "blocked_reason": blocked_reason or None,
476
+ "interrupt_signal": None,
477
+ "transport": "ssh" if target == "remote-linux" else ("wsl" if control_mode == "wsl" else ("powershell" if control_mode == "windows-native" else "direct")),
478
+ "auth_method": auth_method or None,
479
+ "retryable": status in {"blocked", "started"},
480
+ "exit_code": None,
481
+ "http_status": None,
482
+ "created_at": datetime.datetime.utcnow().isoformat(timespec="milliseconds") + "Z",
483
+ }
484
+ try:
485
+ os.makedirs(telemetry_dir, mode=0o700, exist_ok=True)
486
+ os.chmod(telemetry_dir, 0o700)
487
+ event_path = os.path.join(telemetry_dir, "events.jsonl")
488
+ with open(event_path, "a", encoding="utf-8") as stream:
489
+ stream.write(json.dumps(event, separators=(",", ":")) + "\n")
490
+ os.chmod(event_path, 0o600)
491
+ except Exception:
492
+ pass
493
+ print(json.dumps(event, separators=(",", ":")))
494
+ PY
495
+ } 2>/dev/null || true)"
496
+ [[ -n "$payload" ]] || return 0
497
+ (
498
+ curl --silent --show-error --connect-timeout 2 --max-time 3 \
499
+ -X POST "$RAINSKILLS_LIFECYCLE_REPORT_URL" \
500
+ -H 'Content-Type: application/json' \
501
+ -H "Idempotency-Key: $(python3 -c 'import json,sys; print(json.load(sys.stdin)["event_id"])' <<<"$payload")" \
502
+ --data-binary "$payload" >/dev/null 2>&1 || true
503
+ ) &
504
+ }
505
+
314
506
  initialize_rainskills_installation_reporting() {
315
507
  local arg target=""
316
- RAINSKILLS_INSTALL_ATTEMPT_ID="$(new_rainskills_install_attempt_id)"
508
+ if [[ -z "$RAINSKILLS_INSTALL_ATTEMPT_ID" ]]; then
509
+ RAINSKILLS_INSTALL_ATTEMPT_ID="$(new_rainskills_install_attempt_id)"
510
+ fi
317
511
  for arg in "$@"; do
318
512
  case "$arg" in
319
513
  refresh)
320
514
  RAINSKILLS_INSTALL_ACTION="refresh"
321
515
  ;;
516
+ connect)
517
+ RAINSKILLS_INSTALL_ACTION="connect"
518
+ ;;
322
519
  codex|claude|all)
323
520
  target="$arg"
324
521
  ;;
325
522
  esac
326
523
  done
327
524
  RAINSKILLS_INSTALL_CLIENT="$(rainskills_install_client_for_target "$target")"
525
+ report_rainskills_lifecycle_event "bootstrap" "resume" "resume" "started"
328
526
  report_rainskills_installation "started" "started"
329
527
  }
330
528
 
@@ -410,12 +608,14 @@ record_rainskills_authorization() {
410
608
  eid="$(resolve_rainskills_enterprise_id "$base_url" "$token" || true)"
411
609
  [[ -n "$eid" ]] || return 0
412
610
  RAINSKILLS_INSTALL_EID="$eid"
611
+ report_rainskills_lifecycle_event "authorize_legacy" "legacy_callback" "authorize" "completed" "" "" "browser_loopback"
413
612
  report_rainskills_installation "authorized" "success"
414
613
  }
415
614
 
416
615
  set_rainskills_failure_context() {
417
616
  RAINSKILLS_INSTALL_FAILURE_STAGE="$1"
418
617
  RAINSKILLS_INSTALL_FAILURE_CATEGORY="$2"
618
+ RAINSKILLS_TELEMETRY_ERROR_STAGE="$1"
419
619
  }
420
620
 
421
621
  report_unhandled_rainskills_installation_failure() {
@@ -424,6 +624,12 @@ report_unhandled_rainskills_installation_failure() {
424
624
  "${BASH_SUBSHELL:-0}" -eq 0 && \
425
625
  "$RAINSKILLS_INSTALL_TERMINAL_REPORTED" -eq 0 ]]; then
426
626
  RAINSKILLS_INSTALL_TERMINAL_REPORTED=1
627
+ report_rainskills_lifecycle_event \
628
+ "$(rainskills_telemetry_failure_phase)" \
629
+ "resume" \
630
+ "resume" \
631
+ "failed" \
632
+ "$(rainskills_telemetry_error_code)"
427
633
  report_rainskills_installation \
428
634
  "failed" \
429
635
  "failure" \
@@ -561,7 +767,7 @@ copy_skill() {
561
767
 
562
768
  if [[ ! -e "$dest" ]]; then
563
769
  cp -R "$src" "$dest"
564
- log "[install] 已安装到 $dest"
770
+ install_detail_log "[install] 已安装到 $dest"
565
771
  INSTALL_COUNT_NEW=$((INSTALL_COUNT_NEW + 1))
566
772
  return 0
567
773
  fi
@@ -569,13 +775,13 @@ copy_skill() {
569
775
  if [[ "$FORCE" -eq 1 ]]; then
570
776
  rm -rf "$dest"
571
777
  cp -R "$src" "$dest"
572
- log "[overwrite] 已强制覆盖 $dest"
778
+ install_detail_log "[overwrite] 已强制覆盖 $dest"
573
779
  INSTALL_COUNT_FORCED=$((INSTALL_COUNT_FORCED + 1))
574
780
  return 0
575
781
  fi
576
782
 
577
783
  if diff -rq "$src" "$dest" >/dev/null 2>&1; then
578
- log "[skip] $dest 已是最新"
784
+ install_detail_log "[skip] $dest 已是最新"
579
785
  INSTALL_COUNT_UNCHANGED=$((INSTALL_COUNT_UNCHANGED + 1))
580
786
  return 0
581
787
  fi
@@ -585,7 +791,7 @@ copy_skill() {
585
791
  changed_files="$( { diff -rq "$src" "$dest" 2>/dev/null || true; } | wc -l | tr -d ' ')"
586
792
  rm -rf "$dest"
587
793
  cp -R "$src" "$dest"
588
- log "[update] 已更新 ${dest}(${changed_files} 项变化,本地修改已被覆盖)"
794
+ install_detail_log "[update] 已更新 ${dest}(${changed_files} 项变化,本地修改已被覆盖)"
589
795
  INSTALL_COUNT_UPDATED=$((INSTALL_COUNT_UPDATED + 1))
590
796
  }
591
797
 
@@ -596,6 +802,10 @@ parse_args() {
596
802
  ACTION="refresh"
597
803
  shift
598
804
  ;;
805
+ connect)
806
+ ACTION="connect"
807
+ shift
808
+ ;;
599
809
  claude|codex|all)
600
810
  TARGET="$1"
601
811
  shift
@@ -612,6 +822,10 @@ parse_args() {
612
822
  FORCE=1
613
823
  shift
614
824
  ;;
825
+ --verbose)
826
+ RAINSKILLS_VERBOSE=1
827
+ shift
828
+ ;;
615
829
  --skip-mcp)
616
830
  SKIP_MCP=1
617
831
  shift
@@ -669,6 +883,109 @@ parse_args() {
669
883
  done
670
884
  }
671
885
 
886
+ valid_runtime_operation_id() {
887
+ [[ "${1:-}" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89aAbB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$ ]]
888
+ }
889
+
890
+ assert_internal_connect_entry() {
891
+ [[ "${1:-}" == "connect" ]] || return 0
892
+ if [[ "${RAINSKILLS_RUNTIME_CONNECT_COMPLETION:-0}" != "1" ]] \
893
+ || ! valid_runtime_operation_id "${RAINSKILLS_RUNTIME_OPERATION_ID:-}"; then
894
+ printf '错误:runtime connect 必须通过 Rainskills Node 入口执行。\n' >&2
895
+ return 1
896
+ fi
897
+
898
+ shift
899
+ local target="" environment_kind="" console_origin="" mode=""
900
+ while [[ $# -gt 0 ]]; do
901
+ case "$1" in
902
+ codex|claude|all)
903
+ [[ -z "$target" ]] || return 1
904
+ target="$1"
905
+ shift
906
+ ;;
907
+ --saas)
908
+ [[ -z "$mode" ]] || return 1
909
+ mode="saas"
910
+ environment_kind="saas"
911
+ console_origin="$SAAS_DEFAULT_URL"
912
+ shift
913
+ ;;
914
+ --self-hosted)
915
+ [[ -z "$mode" ]] || return 1
916
+ mode="self-hosted"
917
+ environment_kind="private"
918
+ shift
919
+ ;;
920
+ --rainbond-url)
921
+ [[ $# -ge 2 && -z "$console_origin" ]] || return 1
922
+ console_origin="$2"
923
+ shift 2
924
+ ;;
925
+ --allow-insecure-http)
926
+ shift
927
+ ;;
928
+ --non-interactive)
929
+ shift
930
+ ;;
931
+ *)
932
+ return 1
933
+ ;;
934
+ esac
935
+ done
936
+ [[ -n "$target" && -n "$environment_kind" && -n "$console_origin" ]] || return 1
937
+ if [[ "$mode" == "saas" ]]; then
938
+ [[ "$console_origin" == "$SAAS_DEFAULT_URL" ]] || return 1
939
+ else
940
+ assert_canonical_runtime_console_origin "$console_origin" || return 1
941
+ fi
942
+ node "$SCRIPT_DIR/bin/rainskills.js" runtime assert-connect \
943
+ --onboarding-id "$RAINSKILLS_RUNTIME_OPERATION_ID" \
944
+ --target "$target" \
945
+ --environment-kind "$environment_kind" \
946
+ --console-origin "$console_origin" >/dev/null 2>&1 \
947
+ || {
948
+ printf '错误:runtime connect 内部门禁验证失败。\n' >&2
949
+ return 1
950
+ }
951
+ }
952
+
953
+ assert_canonical_runtime_console_origin() {
954
+ local value="$1"
955
+ python3 - "$value" <<'PY' >/dev/null 2>&1
956
+ import sys
957
+ from urllib.parse import urlsplit
958
+
959
+ value = sys.argv[1]
960
+ if not value or value.strip() != value or len(value) > 2048:
961
+ raise SystemExit(1)
962
+ try:
963
+ parsed = urlsplit(value)
964
+ port = parsed.port
965
+ except Exception:
966
+ raise SystemExit(1)
967
+ if (
968
+ parsed.scheme not in ("http", "https")
969
+ or not parsed.hostname
970
+ or parsed.username is not None
971
+ or parsed.password is not None
972
+ or parsed.path not in ("", "/")
973
+ or parsed.query
974
+ or parsed.fragment
975
+ ):
976
+ raise SystemExit(1)
977
+ host = parsed.hostname.lower()
978
+ if ":" in host:
979
+ host = "[{}]".format(host)
980
+ default_port = 80 if parsed.scheme == "http" else 443
981
+ origin = "{}://{}".format(parsed.scheme, host)
982
+ if port is not None and port != default_port:
983
+ origin += ":{}".format(port)
984
+ if value.rstrip("/") != origin:
985
+ raise SystemExit(1)
986
+ PY
987
+ }
988
+
672
989
  resolve_target() {
673
990
  if [[ -n "$CUSTOM_DEST" ]]; then
674
991
  return 0
@@ -1522,7 +1839,8 @@ device_flow_login_to_rainbond() {
1522
1839
  fi
1523
1840
  [[ "$request_status" -eq 0 ]] || return "$request_status"
1524
1841
 
1525
- printf '\nRainbond 设备授权\n' >&2
1842
+ printf '\n[RAINSKILLS_USER_MESSAGE_BEGIN:runtime.device-authorization]\n' >&2
1843
+ printf 'Rainbond 设备授权\n' >&2
1526
1844
  printf '授权码:%s\n' "$DEVICE_FLOW_USER_CODE" >&2
1527
1845
  printf '授权地址:%s\n' "$DEVICE_FLOW_VERIFICATION_URI_COMPLETE" >&2
1528
1846
  printf '终端正在等待授权结果,完成后会自动继续,Ctrl+C 可取消。\n' >&2
@@ -1532,6 +1850,7 @@ device_flow_login_to_rainbond() {
1532
1850
  else
1533
1851
  printf '请在任意能够访问该 Rainbond 平台的电脑上打开上面的地址并完成登录授权。\n' >&2
1534
1852
  fi
1853
+ printf '[RAINSKILLS_USER_MESSAGE_END:runtime.device-authorization]\n' >&2
1535
1854
 
1536
1855
  if ! poll_device_authorization "$base_url"; then
1537
1856
  cleanup_device_flow
@@ -1772,6 +2091,51 @@ configure_shell_autoload() {
1772
2091
  log "[update] 已更新 $rc_file"
1773
2092
  }
1774
2093
 
2094
+ rainskills_package_spec() {
2095
+ local package_version
2096
+ package_version="$(node -p 'require(process.argv[1]).version' "$SCRIPT_DIR/package.json")"
2097
+ [[ "$package_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] \
2098
+ || die "Rainskills package version 无效"
2099
+ printf 'rainskills@%s\n' "$package_version"
2100
+ }
2101
+
2102
+ codex_local_mcp_matches() {
2103
+ local package_spec="$1"
2104
+ local config_file="$HOME/.codex/config.toml"
2105
+ [[ -f "$config_file" ]] || return 1
2106
+ python3 - "$config_file" "$package_spec" <<'PY' >/dev/null
2107
+ import re
2108
+ import sys
2109
+
2110
+ content = open(sys.argv[1], encoding="utf-8").read()
2111
+ match = re.search(r'(?ms)^\[mcp_servers\.rainbond\]\s*(.*?)(?=^\[|\Z)', content)
2112
+ if not match:
2113
+ raise SystemExit(1)
2114
+ block = match.group(1)
2115
+ expected = ["--yes", sys.argv[2], "mcp", "serve", "--client", "codex"]
2116
+ if re.search(r'^command\s*=\s*"npx"\s*$', block, re.M) and all(re.escape('"{}"'.format(v)) in block for v in expected):
2117
+ raise SystemExit(0)
2118
+ raise SystemExit(1)
2119
+ PY
2120
+ }
2121
+
2122
+ claude_local_mcp_matches() {
2123
+ local package_spec="$1"
2124
+ local config_file="$HOME/.claude.json"
2125
+ [[ -f "$config_file" ]] || return 1
2126
+ python3 - "$config_file" "$package_spec" <<'PY' >/dev/null
2127
+ import json
2128
+ import sys
2129
+
2130
+ payload = json.load(open(sys.argv[1], encoding="utf-8"))
2131
+ server = ((payload.get("mcpServers") or {}).get("rainbond") or {})
2132
+ expected = ["--yes", sys.argv[2], "mcp", "serve", "--client", "claude"]
2133
+ if server.get("command") == "npx" and server.get("args") == expected:
2134
+ raise SystemExit(0)
2135
+ raise SystemExit(1)
2136
+ PY
2137
+ }
2138
+
1775
2139
  codex_config_matches() {
1776
2140
  local mcp_url="$1"
1777
2141
  local config_file="$HOME/.codex/config.toml"
@@ -1831,7 +2195,13 @@ PY
1831
2195
  }
1832
2196
 
1833
2197
  configure_codex_mcp() {
1834
- local mcp_url="$1"
2198
+ local package_spec
2199
+ package_spec="$(rainskills_package_spec)"
2200
+
2201
+ if codex_local_mcp_matches "$package_spec"; then
2202
+ log "[configure] Codex 已连接 Rainskills 本地 MCP"
2203
+ return 0
2204
+ fi
1835
2205
 
1836
2206
  if ! command -v codex >/dev/null 2>&1; then
1837
2207
  warn "未找到 Codex CLI,跳过 Codex MCP 配置。"
@@ -1840,14 +2210,20 @@ configure_codex_mcp() {
1840
2210
 
1841
2211
  backup_file "$HOME/.codex/config.toml"
1842
2212
  codex mcp remove rainbond >/dev/null 2>&1 || true
1843
- if ! codex mcp add rainbond --url "$mcp_url" --bearer-token-env-var RAINBOND_JWT >/dev/null; then
2213
+ if ! codex mcp add rainbond -- npx --yes "$package_spec" mcp serve --client codex >/dev/null; then
1844
2214
  return 1
1845
2215
  fi
1846
2216
  log "[configure] 已配置 Codex MCP"
1847
2217
  }
1848
2218
 
1849
2219
  configure_claude_mcp() {
1850
- local mcp_url="$1"
2220
+ local package_spec
2221
+ package_spec="$(rainskills_package_spec)"
2222
+
2223
+ if claude_local_mcp_matches "$package_spec"; then
2224
+ log "[configure] Claude 已连接 Rainskills 本地 MCP"
2225
+ return 0
2226
+ fi
1851
2227
 
1852
2228
  if ! command -v claude >/dev/null 2>&1; then
1853
2229
  warn "未找到 Claude CLI,跳过 Claude MCP 配置。"
@@ -1856,7 +2232,7 @@ configure_claude_mcp() {
1856
2232
 
1857
2233
  backup_file "$HOME/.claude.json"
1858
2234
  claude mcp remove --scope user rainbond >/dev/null 2>&1 || true
1859
- if ! claude mcp add --scope user --transport http rainbond "$mcp_url" -H 'Authorization: GRJWT ${RAINBOND_JWT}' >/dev/null; then
2235
+ if ! claude mcp add --scope user rainbond -- npx --yes "$package_spec" mcp serve --client claude >/dev/null; then
1860
2236
  return 1
1861
2237
  fi
1862
2238
  log "[configure] 已配置 Claude MCP"
@@ -1897,13 +2273,46 @@ validate_mcp_connectivity() {
1897
2273
  fi
1898
2274
 
1899
2275
  if [[ ! "$http_code" =~ ^2 ]]; then
1900
- cleanup_mcp_validation
1901
2276
  if [[ "$http_code" == "404" ]]; then
1902
- local mcp_path
1903
- mcp_path="${mcp_url#*://}"
1904
- mcp_path="/${mcp_path#*/}"
1905
- die "Rainbond MCP 校验失败:${mcp_url} 未暴露 ${mcp_path}。登录已成功,说明这个环境可达,但当前部署的 Rainbond Console 可能未包含 RainSkills MCP 接口,或你连接到了错误的 Rainbond 主机。"
2277
+ if python3 - "$response_file" "$header_file" <<'PY' >/dev/null; then
2278
+ import json
2279
+ import sys
2280
+
2281
+ body_path, header_path = sys.argv[1:]
2282
+ with open(body_path, "r", encoding="utf-8", errors="replace") as fh:
2283
+ body = fh.read(65537)
2284
+ if len(body.encode("utf-8")) > 65536:
2285
+ raise SystemExit(1)
2286
+ with open(header_path, "r", encoding="utf-8", errors="replace") as fh:
2287
+ headers = fh.read().lower()
2288
+
2289
+ content_type = ""
2290
+ for line in headers.splitlines():
2291
+ if line.startswith("content-type:"):
2292
+ content_type = line.split(":", 1)[1].strip()
2293
+ trimmed = body.strip()
2294
+ verified = False
2295
+ if "text/plain" in content_type:
2296
+ verified = trimmed == "Not Found"
2297
+ elif "text/html" in content_type:
2298
+ lower = trimmed.lower()
2299
+ verified = "<title" in lower and "not found" in lower
2300
+ elif "json" in content_type:
2301
+ try:
2302
+ payload = json.loads(trimmed)
2303
+ except Exception:
2304
+ payload = None
2305
+ if isinstance(payload, dict):
2306
+ verified = any(payload.get(key) in (404, "404") for key in (
2307
+ "code", "status", "status_code", "error_code"
2308
+ ))
2309
+ raise SystemExit(0 if verified else 1)
2310
+ PY
2311
+ cleanup_mcp_validation
2312
+ return 2
2313
+ fi
1906
2314
  fi
2315
+ cleanup_mcp_validation
1907
2316
  die "Rainbond MCP 校验失败,HTTP 状态码 ${http_code}"
1908
2317
  fi
1909
2318
 
@@ -1971,11 +2380,43 @@ PY
1971
2380
  log "[verify] Rainbond MCP 可访问"
1972
2381
  }
1973
2382
 
2383
+ select_compatible_mcp_endpoint() {
2384
+ local preferred_url="$1"
2385
+ local generic_url="$2"
2386
+ local token="$3"
2387
+ local validation_status
2388
+ SELECTED_MCP_URL=""
2389
+
2390
+ if validate_mcp_connectivity "$preferred_url" "$token"; then
2391
+ SELECTED_MCP_URL="$preferred_url"
2392
+ return 0
2393
+ else
2394
+ validation_status=$?
2395
+ fi
2396
+ if [[ "$validation_status" -ne 2 ]]; then
2397
+ return "$validation_status"
2398
+ fi
2399
+
2400
+ log "[compat] 当前 Rainbond 未提供 RainSkills 专用 MCP 地址,尝试兼容 MCP 地址。"
2401
+ if validate_mcp_connectivity "$generic_url" "$token"; then
2402
+ SELECTED_MCP_URL="$generic_url"
2403
+ return 0
2404
+ else
2405
+ validation_status=$?
2406
+ fi
2407
+ if [[ "$validation_status" -eq 2 ]]; then
2408
+ die "当前 Rainbond 未提供可用的 MCP 接口,请升级 Rainbond 后重试。"
2409
+ fi
2410
+ return "$validation_status"
2411
+ }
2412
+
1974
2413
  migrate_codex_mcp_if_generic() {
1975
2414
  local generic_url="$1"
1976
2415
  local dedicated_url="$2"
1977
2416
  local config_file="$HOME/.codex/config.toml"
1978
- codex_config_matches "$generic_url" || return 0
2417
+ if ! codex_config_matches "$generic_url" && ! codex_config_matches "$dedicated_url"; then
2418
+ return 0
2419
+ fi
1979
2420
 
1980
2421
  local backup="${config_file}.rainskills-backup"
1981
2422
  cp "$config_file" "$backup"
@@ -1985,14 +2426,16 @@ migrate_codex_mcp_if_generic() {
1985
2426
  warn "Codex MCP 迁移失败,已恢复原配置。"
1986
2427
  return 1
1987
2428
  fi
1988
- log "[migrate] Codex MCP 已切换到 RainSkills 专用地址"
2429
+ log "[migrate] Codex MCP 已切换到 Rainskills 本地路由"
1989
2430
  }
1990
2431
 
1991
2432
  migrate_claude_mcp_if_generic() {
1992
2433
  local generic_url="$1"
1993
2434
  local dedicated_url="$2"
1994
2435
  local config_file="$HOME/.claude.json"
1995
- claude_config_matches "$generic_url" || return 0
2436
+ if ! claude_config_matches "$generic_url" && ! claude_config_matches "$dedicated_url"; then
2437
+ return 0
2438
+ fi
1996
2439
 
1997
2440
  local backup="${config_file}.rainskills-backup"
1998
2441
  cp "$config_file" "$backup"
@@ -2002,7 +2445,7 @@ migrate_claude_mcp_if_generic() {
2002
2445
  warn "Claude MCP 迁移失败,已恢复原配置。"
2003
2446
  return 1
2004
2447
  fi
2005
- log "[migrate] Claude MCP 已切换到 RainSkills 专用地址"
2448
+ log "[migrate] Claude MCP 已切换到 Rainskills 本地路由"
2006
2449
  }
2007
2450
 
2008
2451
  looks_like_jwt() {
@@ -2194,6 +2637,7 @@ read_cached_rainbond_url() {
2194
2637
 
2195
2638
  do_refresh() {
2196
2639
  set_rainskills_failure_context "authorization" "authorization_failed"
2640
+ report_rainskills_lifecycle_event "authorize_legacy" "legacy_callback" "authorize" "started" "" "" "browser_loopback"
2197
2641
  ensure_python3
2198
2642
 
2199
2643
  # Refresh exists because the cached JWT is broken; ignore inherited tokens
@@ -2235,9 +2679,10 @@ do_refresh() {
2235
2679
  base_url="$(normalize_rainbond_url "$base_url_input")"
2236
2680
  confirm_insecure_http_if_needed "$base_url"
2237
2681
 
2238
- local token generic_mcp_url codex_mcp_url claude_mcp_url
2682
+ local token generic_mcp_url codex_mcp_url claude_mcp_url selected_codex_mcp_url selected_claude_mcp_url
2239
2683
  local migrate_codex=0 migrate_claude=0
2240
2684
  local validate_codex=0 validate_claude=0
2685
+ local using_legacy_compat=0
2241
2686
  obtain_rainbond_token "$base_url" "$DEPLOYMENT_MODE_INPUT"
2242
2687
  token="$OBTAINED_RAINBOND_TOKEN"
2243
2688
  generic_mcp_url="${base_url}/console/mcp/query"
@@ -2272,11 +2717,15 @@ do_refresh() {
2272
2717
 
2273
2718
  set_rainskills_failure_context "verification" "mcp_verification_failed"
2274
2719
  if (( validate_codex == 1 )); then
2275
- validate_mcp_connectivity "$codex_mcp_url" "$token"
2720
+ select_compatible_mcp_endpoint "$codex_mcp_url" "$generic_mcp_url" "$token"
2721
+ selected_codex_mcp_url="$SELECTED_MCP_URL"
2722
+ [[ "$selected_codex_mcp_url" != "$generic_mcp_url" ]] || using_legacy_compat=1
2276
2723
  token="$VALIDATED_TOKEN"
2277
2724
  fi
2278
2725
  if (( validate_claude == 1 )); then
2279
- validate_mcp_connectivity "$claude_mcp_url" "$token"
2726
+ select_compatible_mcp_endpoint "$claude_mcp_url" "$generic_mcp_url" "$token"
2727
+ selected_claude_mcp_url="$SELECTED_MCP_URL"
2728
+ [[ "$selected_claude_mcp_url" != "$generic_mcp_url" ]] || using_legacy_compat=1
2280
2729
  token="$VALIDATED_TOKEN"
2281
2730
  fi
2282
2731
 
@@ -2285,16 +2734,21 @@ do_refresh() {
2285
2734
  configure_shell_autoload
2286
2735
 
2287
2736
  set_rainskills_failure_context "configuration" "mcp_configuration_failed"
2288
- if (( migrate_codex == 1 )); then
2737
+ report_rainskills_lifecycle_event "configure_mcp" "configure_mcp" "configure_mcp" "started"
2738
+ if (( validate_codex == 1 )); then
2289
2739
  migrate_codex_mcp_if_generic "$generic_mcp_url" "$codex_mcp_url" \
2290
- || die "Codex MCP 专用地址迁移失败"
2740
+ || die "Codex MCP 本地路由迁移失败"
2291
2741
  fi
2292
- if (( migrate_claude == 1 )); then
2742
+ if (( validate_claude == 1 )); then
2293
2743
  migrate_claude_mcp_if_generic "$generic_mcp_url" "$claude_mcp_url" \
2294
- || die "Claude MCP 专用地址迁移失败"
2744
+ || die "Claude MCP 本地路由迁移失败"
2295
2745
  fi
2296
2746
  log ""
2297
- log "JWT 刷新完成。脚本管理的旧通用 MCP 地址已按需迁移到 RainSkills 专用地址。"
2747
+ if (( using_legacy_compat == 1 )); then
2748
+ log "JWT 刷新完成。当前 Rainbond 将继续使用兼容 MCP 地址。"
2749
+ else
2750
+ log "JWT 刷新完成。脚本管理的旧 MCP 地址已按需迁移到 Rainskills 本地路由。"
2751
+ fi
2298
2752
  if (( validate_codex == 1 || validate_claude == 1 )); then
2299
2753
  log "请重启 Claude Code 或 Codex 让新 JWT 生效(它们在启动时一次性读取 RAINBOND_JWT)。"
2300
2754
  fi
@@ -2302,6 +2756,7 @@ do_refresh() {
2302
2756
  log "如果想立刻在当前终端使用,请执行:source ${ACTIVE_SHELL_RC}"
2303
2757
  fi
2304
2758
  RAINSKILLS_INSTALL_TERMINAL_REPORTED=1
2759
+ report_rainskills_lifecycle_event "configure_mcp" "configure_mcp" "configure_mcp" "completed"
2305
2760
  report_rainskills_installation "configured" "success"
2306
2761
  }
2307
2762
 
@@ -2316,6 +2771,7 @@ configure_mcp() {
2316
2771
  fi
2317
2772
 
2318
2773
  set_rainskills_failure_context "authorization" "authorization_failed"
2774
+ report_rainskills_lifecycle_event "authorize_legacy" "legacy_callback" "authorize" "started" "" "" "browser_loopback"
2319
2775
  ensure_python3
2320
2776
  resolve_deployment_mode
2321
2777
 
@@ -2354,54 +2810,67 @@ configure_mcp() {
2354
2810
  base_url="$(normalize_rainbond_url "$base_url_input")"
2355
2811
  confirm_insecure_http_if_needed "$base_url"
2356
2812
 
2357
- local token codex_mcp_url claude_mcp_url
2813
+ local token generic_mcp_url codex_mcp_url claude_mcp_url selected_codex_mcp_url selected_claude_mcp_url
2358
2814
  obtain_rainbond_token "$base_url" "$DEPLOYMENT_MODE_INPUT"
2359
2815
  token="$OBTAINED_RAINBOND_TOKEN"
2360
2816
  RAINSKILLS_INSTALL_CLIENT="$(rainskills_install_client_for_target "$TARGET")"
2361
2817
  record_rainskills_authorization "$base_url" "$token"
2818
+ generic_mcp_url="${base_url}/console/mcp/query"
2362
2819
  codex_mcp_url="${base_url}/console/mcp/rainskills/codex/query"
2363
2820
  claude_mcp_url="${base_url}/console/mcp/rainskills/claude-code/query"
2364
2821
 
2365
2822
  set_rainskills_failure_context "verification" "mcp_verification_failed"
2366
2823
  case "$TARGET" in
2367
2824
  codex)
2368
- validate_mcp_connectivity "$codex_mcp_url" "$token"
2825
+ select_compatible_mcp_endpoint "$codex_mcp_url" "$generic_mcp_url" "$token"
2826
+ selected_codex_mcp_url="$SELECTED_MCP_URL"
2369
2827
  token="$VALIDATED_TOKEN"
2370
2828
  ;;
2371
2829
  claude)
2372
- validate_mcp_connectivity "$claude_mcp_url" "$token"
2830
+ select_compatible_mcp_endpoint "$claude_mcp_url" "$generic_mcp_url" "$token"
2831
+ selected_claude_mcp_url="$SELECTED_MCP_URL"
2373
2832
  token="$VALIDATED_TOKEN"
2374
2833
  ;;
2375
2834
  all)
2376
- validate_mcp_connectivity "$codex_mcp_url" "$token"
2835
+ select_compatible_mcp_endpoint "$codex_mcp_url" "$generic_mcp_url" "$token"
2836
+ selected_codex_mcp_url="$SELECTED_MCP_URL"
2377
2837
  token="$VALIDATED_TOKEN"
2378
- validate_mcp_connectivity "$claude_mcp_url" "$token"
2838
+ select_compatible_mcp_endpoint "$claude_mcp_url" "$generic_mcp_url" "$token"
2839
+ selected_claude_mcp_url="$SELECTED_MCP_URL"
2379
2840
  token="$VALIDATED_TOKEN"
2380
2841
  ;;
2381
2842
  esac
2382
2843
 
2383
2844
  # Refresh this process's env for any downstream CLI behavior that resolves it.
2384
2845
  export RAINBOND_JWT="$token"
2385
- write_token_file "$token" "$base_url"
2386
- configure_shell_autoload
2846
+ if [[ "$ACTION" == "connect" ]]; then
2847
+ node "$SCRIPT_DIR/bin/rainskills.js" runtime persist-connect-credential \
2848
+ --onboarding-id "$RAINSKILLS_RUNTIME_OPERATION_ID" >/dev/null \
2849
+ || die "runtime connect 凭据安全写入失败。"
2850
+ else
2851
+ write_token_file "$token" "$base_url"
2852
+ configure_shell_autoload
2853
+ fi
2387
2854
 
2388
2855
  set_rainskills_failure_context "configuration" "mcp_configuration_failed"
2856
+ report_rainskills_lifecycle_event "configure_mcp" "configure_mcp" "configure_mcp" "started"
2389
2857
  local configured=0
2390
2858
  case "$TARGET" in
2391
2859
  codex)
2392
- configure_codex_mcp "$codex_mcp_url" && configured=1 || true
2860
+ configure_codex_mcp && configured=1 || true
2393
2861
  ;;
2394
2862
  claude)
2395
- configure_claude_mcp "$claude_mcp_url" && configured=1 || true
2863
+ configure_claude_mcp && configured=1 || true
2396
2864
  ;;
2397
2865
  all)
2398
- configure_codex_mcp "$codex_mcp_url" && configured=$((configured + 1)) || true
2399
- configure_claude_mcp "$claude_mcp_url" && configured=$((configured + 1)) || true
2866
+ configure_codex_mcp && configured=$((configured + 1)) || true
2867
+ configure_claude_mcp && configured=$((configured + 1)) || true
2400
2868
  ;;
2401
2869
  esac
2402
2870
 
2403
2871
  (( configured > 0 )) || die "所选平台都未能完成 MCP 配置。"
2404
2872
  RAINSKILLS_INSTALL_TERMINAL_REPORTED=1
2873
+ report_rainskills_lifecycle_event "configure_mcp" "configure_mcp" "configure_mcp" "completed"
2405
2874
  report_rainskills_installation "configured" "success"
2406
2875
 
2407
2876
  if [[ -n "$ACTIVE_SHELL_RC" ]]; then
@@ -2418,6 +2887,24 @@ main() {
2418
2887
  return 0
2419
2888
  fi
2420
2889
 
2890
+ if [[ "$ACTION" == "connect" ]]; then
2891
+ [[ "${RAINSKILLS_RUNTIME_CONNECT_COMPLETION:-0}" == "1" ]] \
2892
+ && valid_runtime_operation_id "${RAINSKILLS_RUNTIME_OPERATION_ID:-}" \
2893
+ || die "runtime connect 必须通过 Rainskills Node 入口执行。"
2894
+ if [[ "$DEPLOYMENT_MODE_INPUT" == "self-hosted" ]]; then
2895
+ assert_canonical_runtime_console_origin "$RAINBOND_URL_INPUT" \
2896
+ || die "Rainbond Console 地址必须是已经安全校验的规范 origin。"
2897
+ fi
2898
+ ALLOW_INSECURE_HTTP=1
2899
+ resolve_target
2900
+ RAINSKILLS_INSTALL_CLIENT="$(rainskills_install_client_for_target "$TARGET")"
2901
+ configure_mcp
2902
+ if [[ "${RAINSKILLS_RUNTIME_CONNECT_COMPLETION:-0}" == "1" ]]; then
2903
+ node "$SCRIPT_DIR/bin/rainskills.js" runtime complete-connect \
2904
+ --onboarding-id "$RAINSKILLS_RUNTIME_OPERATION_ID"
2905
+ fi
2906
+ return 0
2907
+ fi
2421
2908
  resolve_target
2422
2909
  RAINSKILLS_INSTALL_CLIENT="$(rainskills_install_client_for_target "$TARGET")"
2423
2910
  set_rainskills_failure_context "skill_installation" "skill_installation_failed"
@@ -2440,34 +2927,20 @@ main() {
2440
2927
  done < <(collect_destinations)
2441
2928
 
2442
2929
  for dest in "${destinations[@]}"; do
2443
- log "安装到:$dest"
2930
+ install_detail_log "安装到:$dest"
2444
2931
  for skill_dir in "${skills[@]}"; do
2445
2932
  copy_skill "$skill_dir" "$dest"
2446
2933
  done
2447
2934
  done
2448
2935
 
2449
- configure_mcp
2450
-
2451
- if [[ "$RAINSKILLS_INSTALL_DEFERRED" -eq 1 ]]; then
2452
- return 0
2453
- fi
2454
-
2455
- log ""
2456
- log "安装完成。本次:${INSTALL_COUNT_NEW} 项新装 / ${INSTALL_COUNT_UPDATED} 项已更新 / ${INSTALL_COUNT_UNCHANGED} 项已是最新 / ${INSTALL_COUNT_FORCED} 项强制覆盖"
2457
- case "$TARGET" in
2458
- codex)
2459
- log "请重新加载 shell 环境并重启 Codex 以加载新技能和 MCP。"
2460
- ;;
2461
- claude)
2462
- log "请重新加载 shell 环境并重启 Claude Code 以加载新技能和 MCP。"
2463
- ;;
2464
- all)
2465
- log "请重启 Codex / Claude Code 以加载新技能和 MCP。"
2466
- ;;
2467
- esac
2936
+ install_detail_log ""
2937
+ install_detail_log "安装完成。本次:${INSTALL_COUNT_NEW} 项新装 / ${INSTALL_COUNT_UPDATED} 项已更新 / ${INSTALL_COUNT_UNCHANGED} 项已是最新 / ${INSTALL_COUNT_FORCED} 项强制覆盖"
2938
+ install_detail_log ""
2939
+ print_capability_summary
2468
2940
  }
2469
2941
 
2470
2942
  if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
2943
+ assert_internal_connect_entry "$@"
2471
2944
  trap 'handle_installer_signal 130' INT
2472
2945
  trap 'handle_installer_signal 143' TERM
2473
2946
  trap 'handle_installer_exit "$?"' EXIT