@tencent-rtc/trtc-agent-skills 0.1.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/LICENSE +21 -0
- package/README.md +172 -0
- package/README.zh.md +173 -0
- package/bin/cli.js +434 -0
- package/knowledge-base/index.yaml +454 -0
- package/knowledge-base/platform-slice-template.md +233 -0
- package/knowledge-base/scenario-spec.md +350 -0
- package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
- package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
- package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
- package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
- package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
- package/knowledge-base/slice-spec.md +546 -0
- package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
- package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
- package/knowledge-base/slices/conference/web/device-control.md +338 -0
- package/knowledge-base/slices/conference/web/login-auth.md +261 -0
- package/knowledge-base/slices/conference/web/network-quality.md +190 -0
- package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
- package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
- package/knowledge-base/slices/conference/web/participant-list.md +238 -0
- package/knowledge-base/slices/conference/web/participant-management.md +718 -0
- package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
- package/knowledge-base/slices/conference/web/room-call.md +213 -0
- package/knowledge-base/slices/conference/web/room-chat.md +426 -0
- package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
- package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
- package/knowledge-base/slices/conference/web/screen-share.md +211 -0
- package/knowledge-base/slices/conference/web/video-layout.md +675 -0
- package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
- package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
- package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
- package/knowledge-base/slices/live/anchor-preview.md +90 -0
- package/knowledge-base/slices/live/anchor-room-config.md +104 -0
- package/knowledge-base/slices/live/audience-list.md +86 -0
- package/knowledge-base/slices/live/audience-manage.md +92 -0
- package/knowledge-base/slices/live/audience-watch.md +85 -0
- package/knowledge-base/slices/live/audio.md +116 -0
- package/knowledge-base/slices/live/barrage.md +88 -0
- package/knowledge-base/slices/live/beauty.md +99 -0
- package/knowledge-base/slices/live/coguest-apply.md +105 -0
- package/knowledge-base/slices/live/device-control.md +91 -0
- package/knowledge-base/slices/live/error-codes.md +167 -0
- package/knowledge-base/slices/live/gift.md +84 -0
- package/knowledge-base/slices/live/ios/.gitkeep +0 -0
- package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
- package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
- package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
- package/knowledge-base/slices/live/ios/audience-list.md +353 -0
- package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
- package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
- package/knowledge-base/slices/live/ios/audio.md +373 -0
- package/knowledge-base/slices/live/ios/barrage.md +285 -0
- package/knowledge-base/slices/live/ios/beauty.md +323 -0
- package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
- package/knowledge-base/slices/live/ios/device-control.md +286 -0
- package/knowledge-base/slices/live/ios/error-codes.md +270 -0
- package/knowledge-base/slices/live/ios/gift.md +315 -0
- package/knowledge-base/slices/live/ios/live-list.md +269 -0
- package/knowledge-base/slices/live/ios/login-auth.md +247 -0
- package/knowledge-base/slices/live/live-list.md +82 -0
- package/knowledge-base/slices/live/login-auth.md +78 -0
- package/package.json +34 -0
- package/skills/trtc/SKILL.md +326 -0
- package/skills/trtc/room-builder/SKILL.md +138 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
- package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
- package/skills/trtc-apply/SKILL.md +97 -0
- package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
- package/skills/trtc-docs/SKILL.md +207 -0
- package/skills/trtc-onboarding/SKILL.md +839 -0
- package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
- package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
- package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
- package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
- package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
- package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
- package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
- package/skills/trtc-search/SKILL.md +221 -0
- package/skills/trtc-topic/SKILL.md +638 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
- package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
- package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
- package/skills/trtc-topic/references/execution-units.yaml +58 -0
- package/skills/trtc-topic/runtime/README.md +50 -0
- package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
- package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
- package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
- package/skills/trtc-topic/runtime/package-lock.json +1211 -0
- package/skills/trtc-topic/runtime/package.json +13 -0
- package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
- package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
- package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
- package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/apply.py +581 -0
- package/skills/trtc-topic/scripts/finalize_session.py +113 -0
- package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
- package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
- package/skills/trtc-topic/scripts/next_slice.py +137 -0
- package/skills/trtc-topic/tests/README.md +70 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/conftest.py +72 -0
- package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
- package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
- package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
- package/skills/trtc-topic/tests/test_gates.py +316 -0
- package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
- package/skills/trtc-topic/tests/test_state_machine.py +414 -0
- package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
- package/skills/trtc-topic/tests/test_topic_skill_invariants.py +130 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trtc-topic-runtime",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Runtime telemetry bridge for TRTC topic skill (Web platform)",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node telemetry-bridge.mjs"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"puppeteer": "^23.0.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// skills/trtc-topic/runtime/telemetry-bridge.mjs
|
|
3
|
+
//
|
|
4
|
+
// Web telemetry bridge for runtime log collection.
|
|
5
|
+
//
|
|
6
|
+
// Two modes:
|
|
7
|
+
// Mode A (default, visible): Launches a visible Chrome window + Vite dev
|
|
8
|
+
// server. User interacts directly in the Puppeteer-managed browser.
|
|
9
|
+
// Mode B (--connect): Connects to user's existing Chrome via CDP. No new
|
|
10
|
+
// browser launched, no Vite started. Attaches to the page matching the URL.
|
|
11
|
+
//
|
|
12
|
+
// In both modes, every page console event is forwarded as raw text to stdout,
|
|
13
|
+
// which telemetry_collector.py pipes into .trtc-telemetry/runtime.log.
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { connect as tcpConnect } from "node:net";
|
|
17
|
+
import process from "node:process";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Argv parsing
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
function parseArgv(argv) {
|
|
23
|
+
const out = {};
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === "--url") out.url = argv[++i];
|
|
27
|
+
else if (a === "--workspace") out.workspace = argv[++i];
|
|
28
|
+
else if (a === "--connect") out.connect = argv[++i];
|
|
29
|
+
}
|
|
30
|
+
const missing = ["url", "workspace"].filter((k) => !out[k]);
|
|
31
|
+
if (missing.length) {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
`telemetry-bridge: missing required args: ${missing.join(", ")}\n` +
|
|
34
|
+
`usage: node telemetry-bridge.mjs --url <u> --workspace <path> [--connect <endpoint>]\n` +
|
|
35
|
+
`\n` +
|
|
36
|
+
`Modes:\n` +
|
|
37
|
+
` (default) Launch visible Chrome + Vite dev server\n` +
|
|
38
|
+
` --connect <endpoint> Connect to existing Chrome via CDP\n` +
|
|
39
|
+
` e.g. --connect http://localhost:9222\n`,
|
|
40
|
+
);
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// TCP port probe
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
function parsePort(url) {
|
|
50
|
+
const m = url.match(/:(\d+)(?:\/|$)/);
|
|
51
|
+
return m ? Number.parseInt(m[1], 10) : 80;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function probeTcp(host, port, timeoutMs) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const socket = tcpConnect({ host, port });
|
|
57
|
+
let done = false;
|
|
58
|
+
const finish = (ok) => {
|
|
59
|
+
if (done) return;
|
|
60
|
+
done = true;
|
|
61
|
+
socket.destroy();
|
|
62
|
+
resolve(ok);
|
|
63
|
+
};
|
|
64
|
+
socket.once("connect", () => finish(true));
|
|
65
|
+
socket.once("error", () => finish(false));
|
|
66
|
+
setTimeout(() => finish(false), timeoutMs);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function findFreePort(host, start, span) {
|
|
71
|
+
for (let p = start; p < start + span; p++) {
|
|
72
|
+
const occupied = await probeTcp(host, p, 200);
|
|
73
|
+
if (!occupied) return p;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function waitForPort(host, port, attempts, intervalMs) {
|
|
79
|
+
for (let i = 0; i < attempts; i++) {
|
|
80
|
+
if (await probeTcp(host, port, intervalMs)) return true;
|
|
81
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Raw log emitter — outputs console text as-is, like Chrome DevTools
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
function emitRaw(line) {
|
|
90
|
+
process.stdout.write(line + "\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Attach console listeners to a page
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
function attachPageListeners(page) {
|
|
97
|
+
page.on("console", (msg) => {
|
|
98
|
+
let text;
|
|
99
|
+
try { text = msg.text(); } catch { text = String(msg); }
|
|
100
|
+
const location = msg.location();
|
|
101
|
+
const source = location && location.url
|
|
102
|
+
? `${location.url.split("/").pop()}:${location.lineNumber ?? 0}`
|
|
103
|
+
: "";
|
|
104
|
+
const prefix = source ? `${source} ` : "";
|
|
105
|
+
emitRaw(`${prefix}${text}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
page.on("pageerror", (err) => {
|
|
109
|
+
const message = err && err.message ? err.message : String(err);
|
|
110
|
+
emitRaw(`[pageerror] ${message}`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
page.on("requestfailed", (req) => {
|
|
114
|
+
const failure = req.failure();
|
|
115
|
+
emitRaw(`[requestfailed] ${req.url()} :: ${failure ? failure.errorText : "unknown"}`);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Mode A: Launch visible browser + Vite dev server
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
async function modeVisible(puppeteer, url, workspace) {
|
|
123
|
+
const host = "localhost";
|
|
124
|
+
const requestedPort = parsePort(url);
|
|
125
|
+
|
|
126
|
+
// Find free port
|
|
127
|
+
const chosenPort = await findFreePort(host, requestedPort, 20);
|
|
128
|
+
if (chosenPort === null) {
|
|
129
|
+
emitRaw(`telemetry-bridge: no free port in [${requestedPort}, ${requestedPort + 20})`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (chosenPort !== requestedPort) {
|
|
133
|
+
emitRaw(`telemetry-bridge: port ${requestedPort} occupied; using ${chosenPort}`);
|
|
134
|
+
}
|
|
135
|
+
const targetUrl = url.replace(`:${requestedPort}`, `:${chosenPort}`);
|
|
136
|
+
|
|
137
|
+
// Spawn Vite dev server
|
|
138
|
+
const vite = spawn("npm", ["run", "dev", "--", "--port", String(chosenPort), "--strictPort"], {
|
|
139
|
+
cwd: workspace,
|
|
140
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
141
|
+
});
|
|
142
|
+
let viteExited = false;
|
|
143
|
+
vite.on("exit", (code, signal) => {
|
|
144
|
+
viteExited = true;
|
|
145
|
+
emitRaw(`telemetry-bridge: vite exited code=${code} signal=${signal || ""}`);
|
|
146
|
+
});
|
|
147
|
+
vite.stdout.resume();
|
|
148
|
+
vite.stderr.on("data", (chunk) => {
|
|
149
|
+
const text = chunk.toString("utf8").trimEnd();
|
|
150
|
+
if (text) emitRaw(`[vite] ${text}`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Wait for dev server
|
|
154
|
+
const ready = await waitForPort(host, chosenPort, 60, 1000);
|
|
155
|
+
if (!ready || viteExited) {
|
|
156
|
+
emitRaw(`telemetry-bridge: vite not ready on port ${chosenPort}`);
|
|
157
|
+
try { vite.kill("SIGTERM"); } catch {}
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Launch VISIBLE browser (headless: false)
|
|
162
|
+
let browser;
|
|
163
|
+
try {
|
|
164
|
+
browser = await puppeteer.launch({
|
|
165
|
+
headless: false,
|
|
166
|
+
args: [
|
|
167
|
+
"--no-sandbox",
|
|
168
|
+
"--disable-dev-shm-usage",
|
|
169
|
+
"--use-fake-ui-for-media-stream",
|
|
170
|
+
"--use-fake-device-for-media-stream",
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
} catch (e) {
|
|
174
|
+
emitRaw(`telemetry-bridge: chromium launch failed: ${e.message}`);
|
|
175
|
+
try { vite.kill("SIGTERM"); } catch {}
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const page = await browser.newPage();
|
|
180
|
+
|
|
181
|
+
// Install error probes before page load
|
|
182
|
+
await page.evaluateOnNewDocument(() => {
|
|
183
|
+
const tag = (probe, payload) =>
|
|
184
|
+
console.log(JSON.stringify({ __probe: probe, ...payload }));
|
|
185
|
+
window.addEventListener("error", (e) => {
|
|
186
|
+
tag("page_error", { message: (e && e.message) || String(e), filename: e && e.filename, lineno: e && e.lineno });
|
|
187
|
+
});
|
|
188
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
189
|
+
const reason = e && e.reason;
|
|
190
|
+
tag("unhandled_rejection", { message: reason && reason.message ? reason.message : String(reason) });
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Attach console listeners
|
|
195
|
+
attachPageListeners(page);
|
|
196
|
+
|
|
197
|
+
// Navigate
|
|
198
|
+
try {
|
|
199
|
+
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
200
|
+
} catch (e) {
|
|
201
|
+
emitRaw(`telemetry-bridge: page.goto failed url=${targetUrl} err=${e.message}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
emitRaw(`telemetry-bridge: [visible mode] page loaded at ${targetUrl}, capturing events...`);
|
|
205
|
+
|
|
206
|
+
// Shutdown handler — ensure Chromium is fully closed before exit
|
|
207
|
+
let shuttingDown = false;
|
|
208
|
+
const shutdown = async () => {
|
|
209
|
+
if (shuttingDown) return;
|
|
210
|
+
shuttingDown = true;
|
|
211
|
+
try { await browser.close(); } catch {}
|
|
212
|
+
try { vite.kill("SIGTERM"); } catch {}
|
|
213
|
+
// Give Vite a moment to exit gracefully, then force-kill
|
|
214
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
215
|
+
try { if (!viteExited) vite.kill("SIGKILL"); } catch {}
|
|
216
|
+
process.exit(0);
|
|
217
|
+
};
|
|
218
|
+
process.on("SIGTERM", shutdown);
|
|
219
|
+
process.on("SIGINT", shutdown);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Mode B: Connect to existing Chrome via CDP
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
async function modeConnect(puppeteer, connectEndpoint, targetUrl) {
|
|
226
|
+
emitRaw(`telemetry-bridge: [connect mode] connecting to ${connectEndpoint}`);
|
|
227
|
+
|
|
228
|
+
// Connect to existing browser
|
|
229
|
+
let browser;
|
|
230
|
+
try {
|
|
231
|
+
if (connectEndpoint.startsWith("ws://") || connectEndpoint.startsWith("wss://")) {
|
|
232
|
+
browser = await puppeteer.connect({ browserWSEndpoint: connectEndpoint });
|
|
233
|
+
} else {
|
|
234
|
+
browser = await puppeteer.connect({ browserURL: connectEndpoint });
|
|
235
|
+
}
|
|
236
|
+
} catch (e) {
|
|
237
|
+
emitRaw(`telemetry-bridge: failed to connect to browser at ${connectEndpoint}: ${e.message}`);
|
|
238
|
+
emitRaw(`Hint: Start Chrome with --remote-debugging-port=9222`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
emitRaw(`telemetry-bridge: connected to browser`);
|
|
243
|
+
|
|
244
|
+
// Find the target page
|
|
245
|
+
const pages = await browser.pages();
|
|
246
|
+
const targetPort = parsePort(targetUrl);
|
|
247
|
+
let page = pages.find((p) => {
|
|
248
|
+
const u = p.url();
|
|
249
|
+
return u.includes(`localhost:${targetPort}`) || u.includes(`127.0.0.1:${targetPort}`);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (!page) {
|
|
253
|
+
// Try matching by full URL
|
|
254
|
+
page = pages.find((p) => p.url().includes(targetUrl));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (!page) {
|
|
258
|
+
emitRaw(`telemetry-bridge: no page found matching ${targetUrl}. Listing open pages:`);
|
|
259
|
+
for (const p of pages) {
|
|
260
|
+
emitRaw(` - ${p.url()}`);
|
|
261
|
+
}
|
|
262
|
+
if (pages.length > 0) {
|
|
263
|
+
emitRaw(`telemetry-bridge: attaching to first available page. Open ${targetUrl} in the browser to capture its events.`);
|
|
264
|
+
page = pages[0];
|
|
265
|
+
} else {
|
|
266
|
+
emitRaw(`telemetry-bridge: no pages found in browser`);
|
|
267
|
+
await browser.disconnect();
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
emitRaw(`telemetry-bridge: found target page: ${page.url()}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Attach console listeners
|
|
275
|
+
attachPageListeners(page);
|
|
276
|
+
|
|
277
|
+
// Also listen for new pages (user might navigate or open new tabs)
|
|
278
|
+
browser.on("targetcreated", async (target) => {
|
|
279
|
+
if (target.type() === "page") {
|
|
280
|
+
try {
|
|
281
|
+
const newPage = await target.page();
|
|
282
|
+
if (newPage) {
|
|
283
|
+
const newUrl = newPage.url();
|
|
284
|
+
if (newUrl.includes(`localhost:${targetPort}`) || newUrl.includes(`127.0.0.1:${targetPort}`)) {
|
|
285
|
+
emitRaw(`telemetry-bridge: new matching page detected: ${newUrl}`);
|
|
286
|
+
attachPageListeners(newPage);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
emitRaw(`telemetry-bridge: [connect mode] capturing events from ${page.url()}...`);
|
|
294
|
+
|
|
295
|
+
// Shutdown handler — disconnect only, don't close user's browser
|
|
296
|
+
let shuttingDown = false;
|
|
297
|
+
const shutdown = async () => {
|
|
298
|
+
if (shuttingDown) return;
|
|
299
|
+
shuttingDown = true;
|
|
300
|
+
emitRaw(`telemetry-bridge: disconnecting (browser stays open)`);
|
|
301
|
+
try { await browser.disconnect(); } catch {}
|
|
302
|
+
setTimeout(() => process.exit(0), 100).unref();
|
|
303
|
+
};
|
|
304
|
+
process.on("SIGTERM", shutdown);
|
|
305
|
+
process.on("SIGINT", shutdown);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Main
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
async function main() {
|
|
312
|
+
const args = parseArgv(process.argv.slice(2));
|
|
313
|
+
const { url, workspace, connect: connectEndpoint } = args;
|
|
314
|
+
|
|
315
|
+
emitRaw(`telemetry-bridge: starting (mode=${connectEndpoint ? "connect" : "visible"})`);
|
|
316
|
+
|
|
317
|
+
// Import Puppeteer
|
|
318
|
+
let puppeteer;
|
|
319
|
+
try {
|
|
320
|
+
puppeteer = (await import("puppeteer")).default;
|
|
321
|
+
} catch (e) {
|
|
322
|
+
emitRaw(`telemetry-bridge: puppeteer import failed: ${e.message}. Run \`cd runtime && npm install\`.`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (connectEndpoint) {
|
|
327
|
+
await modeConnect(puppeteer, connectEndpoint, url);
|
|
328
|
+
} else {
|
|
329
|
+
await modeVisible(puppeteer, url, workspace);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Keep alive — shutdown is driven by SIGTERM from telemetry_collector.py
|
|
333
|
+
setInterval(() => {}, 60_000).unref();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
main().catch((e) => {
|
|
337
|
+
emitRaw(`telemetry-bridge: fatal ${e && e.stack ? e.stack : e}`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""telemetry_collector.py — Start/stop runtime log collection.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python3 telemetry_collector.py --mode start --platform web --workspace /path/to/project
|
|
5
|
+
python3 telemetry_collector.py --mode stop --workspace /path/to/project
|
|
6
|
+
|
|
7
|
+
Starts a platform-specific log stream process in the background, piping its
|
|
8
|
+
stdout to <workspace>/.trtc-telemetry/runtime.log. Stores the PID for later
|
|
9
|
+
stop.
|
|
10
|
+
|
|
11
|
+
On stop: kills processes → filters errors from runtime.log → writes
|
|
12
|
+
runtime_error.log + runtime_context.json for MCP upload.
|
|
13
|
+
"""
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import signal
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
25
|
+
from lib.platforms import discover_devices, log_stream_command
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Error filtering patterns
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
# Match: lines that are actual SDK errors
|
|
33
|
+
_ERROR_PATTERNS = [
|
|
34
|
+
re.compile(r"<ERROR>"),
|
|
35
|
+
re.compile(r"error_code:\s*[1-9]\d*"), # Non-zero error_code
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Exclude: false positives (skip even if _ERROR_PATTERNS match)
|
|
39
|
+
_ERROR_EXCLUSIONS = [
|
|
40
|
+
re.compile(r"telemetry-bridge:"),
|
|
41
|
+
re.compile(r"favicon\.ico.*404"),
|
|
42
|
+
re.compile(r"AudioContext was not allowed"),
|
|
43
|
+
re.compile(r"\[↑t\d\] on error"), # Event listener registration
|
|
44
|
+
re.compile(r"error_code:\s*0"), # Success callback
|
|
45
|
+
re.compile(r"error_message:\s*success"), # Success callback
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Context extraction patterns
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
_CONTEXT_EXTRACTORS = {
|
|
53
|
+
"trtc_web": re.compile(r"TRTC Web SDK Version:\s*([\d.\w-]+)"),
|
|
54
|
+
"trtc_cloud": re.compile(r"TRTCCloud Version:\s*([\d.]+)"),
|
|
55
|
+
"room_engine": re.compile(r"TUIRoomEngine Web SDK Version:\s*([\d.]+)"),
|
|
56
|
+
"chat_engine": re.compile(r"TUIChatEngine-Lite\.VERSION:([\d.]+)"),
|
|
57
|
+
"os": re.compile(r"<INFO> OS:\s*(.+)"),
|
|
58
|
+
"sdk_app_id": re.compile(r"sdkAppId=(\d+)"),
|
|
59
|
+
"user_id": re.compile(r"userId=(\w+)"),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Helpers
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def _telemetry_dir(workspace: Path) -> Path:
|
|
68
|
+
"""Return the .trtc-telemetry directory inside the workspace."""
|
|
69
|
+
d = workspace / ".trtc-telemetry"
|
|
70
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
return d
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _filter_errors(runtime_log: Path, error_log: Path) -> list[str]:
|
|
75
|
+
"""Filter error lines from runtime.log, write to runtime_error.log."""
|
|
76
|
+
if not runtime_log.exists() or runtime_log.stat().st_size == 0:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
errors: list[str] = []
|
|
80
|
+
try:
|
|
81
|
+
with open(runtime_log, "r", encoding="utf-8", errors="replace") as f:
|
|
82
|
+
for line in f:
|
|
83
|
+
line_stripped = line.strip()
|
|
84
|
+
if not line_stripped:
|
|
85
|
+
continue
|
|
86
|
+
# Check exclusions first
|
|
87
|
+
if any(exc.search(line_stripped) for exc in _ERROR_EXCLUSIONS):
|
|
88
|
+
continue
|
|
89
|
+
# Check if line matches any error pattern
|
|
90
|
+
if any(pat.search(line_stripped) for pat in _ERROR_PATTERNS):
|
|
91
|
+
errors.append(line_stripped)
|
|
92
|
+
except OSError:
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
# Write error log (overwrite)
|
|
96
|
+
if errors:
|
|
97
|
+
try:
|
|
98
|
+
error_log.write_text("\n".join(errors) + "\n", encoding="utf-8")
|
|
99
|
+
except OSError:
|
|
100
|
+
pass
|
|
101
|
+
else:
|
|
102
|
+
# Clear previous error log if no errors this run
|
|
103
|
+
try:
|
|
104
|
+
error_log.unlink(missing_ok=True)
|
|
105
|
+
except OSError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
return errors
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _extract_context(runtime_log: Path, context_file: Path) -> None:
|
|
112
|
+
"""Extract environment context from runtime.log, write to runtime_context.json."""
|
|
113
|
+
if not runtime_log.exists() or runtime_log.stat().st_size == 0:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
sdk_versions: dict[str, str] = {}
|
|
117
|
+
context: dict[str, str] = {}
|
|
118
|
+
|
|
119
|
+
version_keys = {"trtc_web", "trtc_cloud", "room_engine", "chat_engine"}
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
with open(runtime_log, "r", encoding="utf-8", errors="replace") as f:
|
|
123
|
+
for line in f:
|
|
124
|
+
for key, pattern in _CONTEXT_EXTRACTORS.items():
|
|
125
|
+
if key in sdk_versions or key in context:
|
|
126
|
+
continue # Already found
|
|
127
|
+
m = pattern.search(line)
|
|
128
|
+
if m:
|
|
129
|
+
value = m.group(1).strip()
|
|
130
|
+
if key in version_keys:
|
|
131
|
+
sdk_versions[key] = value
|
|
132
|
+
else:
|
|
133
|
+
context[key] = value
|
|
134
|
+
# Early exit if all found
|
|
135
|
+
if len(sdk_versions) + len(context) == len(_CONTEXT_EXTRACTORS):
|
|
136
|
+
break
|
|
137
|
+
except OSError:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
result = {}
|
|
141
|
+
if sdk_versions:
|
|
142
|
+
result["sdk_versions"] = sdk_versions
|
|
143
|
+
if context.get("os"):
|
|
144
|
+
result["os"] = context["os"]
|
|
145
|
+
if context.get("sdk_app_id"):
|
|
146
|
+
result["sdk_app_id"] = context["sdk_app_id"]
|
|
147
|
+
if context.get("user_id"):
|
|
148
|
+
result["user_id"] = context["user_id"]
|
|
149
|
+
|
|
150
|
+
if result:
|
|
151
|
+
try:
|
|
152
|
+
context_file.write_text(
|
|
153
|
+
json.dumps(result, indent=2, ensure_ascii=False) + "\n",
|
|
154
|
+
encoding="utf-8",
|
|
155
|
+
)
|
|
156
|
+
except OSError:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Start / Stop
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def _start(platform: str, workspace: Path, connect: str | None = None) -> int:
|
|
165
|
+
"""Start log collection in the background."""
|
|
166
|
+
tel_dir = _telemetry_dir(workspace)
|
|
167
|
+
runtime_log = tel_dir / "runtime.log"
|
|
168
|
+
pid_file = tel_dir / "collector.pid"
|
|
169
|
+
|
|
170
|
+
# Check if already running
|
|
171
|
+
if pid_file.exists():
|
|
172
|
+
pid = int(pid_file.read_text().strip())
|
|
173
|
+
try:
|
|
174
|
+
os.kill(pid, 0) # Check if process exists
|
|
175
|
+
print(json.dumps({
|
|
176
|
+
"status": "already_running",
|
|
177
|
+
"pid": pid,
|
|
178
|
+
}))
|
|
179
|
+
return 0
|
|
180
|
+
except ProcessLookupError:
|
|
181
|
+
pid_file.unlink(missing_ok=True)
|
|
182
|
+
|
|
183
|
+
# Discover device
|
|
184
|
+
devices = discover_devices(platform)
|
|
185
|
+
if not devices:
|
|
186
|
+
print(json.dumps({
|
|
187
|
+
"status": "error",
|
|
188
|
+
"message": f"No {platform} device found.",
|
|
189
|
+
}))
|
|
190
|
+
return 1
|
|
191
|
+
|
|
192
|
+
device = devices[0]
|
|
193
|
+
|
|
194
|
+
# Get log stream command
|
|
195
|
+
try:
|
|
196
|
+
cmd = log_stream_command(platform, device, workspace=workspace, connect=connect)
|
|
197
|
+
except ValueError as e:
|
|
198
|
+
print(json.dumps({"status": "error", "message": str(e)}))
|
|
199
|
+
return 1
|
|
200
|
+
|
|
201
|
+
# Launch in background, stdout → runtime.log (truncate previous)
|
|
202
|
+
f = open(runtime_log, "wb")
|
|
203
|
+
try:
|
|
204
|
+
proc = subprocess.Popen(
|
|
205
|
+
cmd,
|
|
206
|
+
stdout=f,
|
|
207
|
+
stderr=subprocess.STDOUT,
|
|
208
|
+
start_new_session=True, # Create new process group for clean kill
|
|
209
|
+
)
|
|
210
|
+
except FileNotFoundError as e:
|
|
211
|
+
f.close()
|
|
212
|
+
print(json.dumps({"status": "error", "message": f"Command not found: {e}"}))
|
|
213
|
+
return 1
|
|
214
|
+
|
|
215
|
+
pid_file.write_text(str(proc.pid))
|
|
216
|
+
|
|
217
|
+
print(json.dumps({
|
|
218
|
+
"status": "started",
|
|
219
|
+
"pid": proc.pid,
|
|
220
|
+
"platform": platform,
|
|
221
|
+
"device": device.name or device.id,
|
|
222
|
+
}))
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _stop(workspace: Path) -> int:
|
|
227
|
+
"""Stop log collection, filter errors, extract context."""
|
|
228
|
+
tel_dir = _telemetry_dir(workspace)
|
|
229
|
+
pid_file = tel_dir / "collector.pid"
|
|
230
|
+
|
|
231
|
+
if not pid_file.exists():
|
|
232
|
+
print(json.dumps({"status": "not_running"}))
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
pid = int(pid_file.read_text().strip())
|
|
236
|
+
|
|
237
|
+
# Kill the entire process group (node + Chromium + Vite) to avoid orphans
|
|
238
|
+
try:
|
|
239
|
+
os.killpg(pid, signal.SIGTERM)
|
|
240
|
+
except ProcessLookupError:
|
|
241
|
+
pass
|
|
242
|
+
except PermissionError:
|
|
243
|
+
# Fallback: kill just the main process if group kill fails
|
|
244
|
+
try:
|
|
245
|
+
os.kill(pid, signal.SIGTERM)
|
|
246
|
+
except ProcessLookupError:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
# Wait briefly for graceful shutdown (browser.close() in bridge)
|
|
250
|
+
time.sleep(1)
|
|
251
|
+
|
|
252
|
+
# Force-kill any remaining processes in the group
|
|
253
|
+
try:
|
|
254
|
+
os.killpg(pid, signal.SIGKILL)
|
|
255
|
+
except (ProcessLookupError, PermissionError):
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
pid_file.unlink(missing_ok=True)
|
|
259
|
+
|
|
260
|
+
# --- Post-stop: filter errors + extract context ---
|
|
261
|
+
runtime_log = tel_dir / "runtime.log"
|
|
262
|
+
error_log = tel_dir / "runtime_error.log"
|
|
263
|
+
context_file = tel_dir / "runtime_context.json"
|
|
264
|
+
|
|
265
|
+
errors = _filter_errors(runtime_log, error_log)
|
|
266
|
+
_extract_context(runtime_log, context_file)
|
|
267
|
+
|
|
268
|
+
print(json.dumps({
|
|
269
|
+
"status": "stopped",
|
|
270
|
+
"errors_found": len(errors),
|
|
271
|
+
}))
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def main() -> int:
|
|
276
|
+
ap = argparse.ArgumentParser(description="Runtime telemetry collector (start/stop)")
|
|
277
|
+
ap.add_argument("--mode", required=True, choices=["start", "stop"])
|
|
278
|
+
ap.add_argument("--platform", choices=["web", "ios", "android"], default="web")
|
|
279
|
+
ap.add_argument("--workspace", required=True, help="Path to the user's project")
|
|
280
|
+
ap.add_argument("--connect", default=None,
|
|
281
|
+
help="CDP endpoint to connect to existing browser (e.g. http://localhost:9222)")
|
|
282
|
+
args = ap.parse_args()
|
|
283
|
+
|
|
284
|
+
workspace = Path(args.workspace).resolve()
|
|
285
|
+
|
|
286
|
+
if args.mode == "start":
|
|
287
|
+
return _start(args.platform, workspace, connect=args.connect)
|
|
288
|
+
else:
|
|
289
|
+
return _stop(workspace)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
if __name__ == "__main__":
|
|
293
|
+
sys.exit(main())
|