@tencent-rtc/trtc-agent-skills 0.1.0 → 0.1.3
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/.cursor/rules/ui-mode.mdc +99 -0
- package/AGENTS.md +111 -0
- package/CLAUDE.md +133 -0
- package/CODEBUDDY.md +131 -0
- package/README.md +28 -42
- package/README.zh.md +24 -42
- package/ai-instructions/base.md +13 -0
- package/ai-instructions/ui-mode.md +93 -0
- package/bin/cli.js +428 -13
- package/hooks/cursor-adapter.py +315 -0
- package/hooks/hooks-cursor.json +39 -0
- package/hooks/hooks.json +67 -0
- package/knowledge-base/index.yaml +8 -0
- package/knowledge-base/scenarios/conference/base/general-conference.md +6 -3
- package/knowledge-base/slices/conference/web/integration-audit.md +155 -0
- package/knowledge-base/slices/conference/web/login-auth.md +2 -1
- package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +41 -13
- package/knowledge-base/slices/conference/web/prejoin-check.md +8 -5
- package/package.json +7 -1
- package/skills/trtc/SKILL.md +7 -6
- package/skills/trtc/room-builder/SKILL.md +4 -9
- package/skills/trtc/room-builder/assets/local-usersig/basic-info-config.ts +39 -0
- package/skills/trtc/room-builder/assets/local-usersig/lib-generate-test-usersig-es.min.d.ts +4 -0
- package/skills/trtc/room-builder/assets/local-usersig/lib-generate-test-usersig-es.min.js +2 -0
- package/skills/trtc-onboarding/SKILL.md +35 -33
- package/skills/trtc-onboarding/reference/path-a2-integrate.md +101 -57
- package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +139 -68
- package/skills/trtc-onboarding/reference/reporting-protocol.md +2 -0
- package/skills/trtc-onboarding/reference/usersig-handling.md +98 -104
- package/skills/trtc-search/SKILL.md +7 -0
- package/skills/trtc-topic/SKILL.md +26 -42
- 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-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/scripts/__pycache__/apply.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -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
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cursor-adapter.py — Translate Cursor hook protocol to existing Claude-format guardrails.
|
|
3
|
+
|
|
4
|
+
Wired from hooks-cursor.json. Each Cursor hook event runs:
|
|
5
|
+
|
|
6
|
+
python3 ./hooks/cursor-adapter.py <dispatch-key>
|
|
7
|
+
|
|
8
|
+
This script:
|
|
9
|
+
1. Reads the Cursor hook envelope from stdin (camelCase fields).
|
|
10
|
+
2. Normalizes it to the {tool_name, tool_input} shape the existing
|
|
11
|
+
guardrail scripts in skills/*/guardrails/ expect.
|
|
12
|
+
3. Sets CLAUDE_PLUGIN_ROOT and CLAUDE_PROJECT_DIR env vars so the
|
|
13
|
+
existing scripts find sessions and shared libs unchanged.
|
|
14
|
+
4. Spawns the underlying script and propagates stdout/stderr.
|
|
15
|
+
5. Translates exit codes:
|
|
16
|
+
Claude Code: 0=allow, 1=fail, 2=block
|
|
17
|
+
Cursor: 0=success, 2=deny (with JSON), other=fail-open
|
|
18
|
+
We map Claude exit 1 and exit 2 -> Cursor deny (exit 2 + JSON on stdout).
|
|
19
|
+
|
|
20
|
+
Fail-open principle: if anything goes wrong inside the adapter (missing
|
|
21
|
+
script, malformed stdin, dispatch key unknown), we exit 0 silently so a
|
|
22
|
+
broken adapter never blocks the user's workflow.
|
|
23
|
+
|
|
24
|
+
Cursor event mapping (see hooks-cursor.json):
|
|
25
|
+
sessionStart -> trtc-prepare-ui
|
|
26
|
+
beforeReadFile -> gate-slice-read
|
|
27
|
+
preToolUse -> gate-slice-write (filtered to Write/Edit inside)
|
|
28
|
+
afterFileEdit -> verify-ui-post-write, verify-slice-must
|
|
29
|
+
stop -> stop-apply-evidence, trtc-verify-ui, verify-apply-project
|
|
30
|
+
(Claude Code's `Stop` event fires per agent loop
|
|
31
|
+
iteration end. Cursor's `stop` event has the same
|
|
32
|
+
semantic ("Called when the agent loop ends") and
|
|
33
|
+
uniquely supports {"followup_message": "..."} on
|
|
34
|
+
stdout to auto-resubmit a corrective user turn —
|
|
35
|
+
which is exactly what these end-of-task guardrails
|
|
36
|
+
need so the agent can self-correct. Auto-resubmit
|
|
37
|
+
is capped at 5 iterations by Cursor's loop_limit.
|
|
38
|
+
|
|
39
|
+
Historical note: a previous revision of this
|
|
40
|
+
adapter mapped these to `afterAgentResponse`
|
|
41
|
+
because the original author observed `stop` not
|
|
42
|
+
firing. Empirically (Cursor 3.3.8) `stop` does
|
|
43
|
+
fire reliably; the original "stop never fires"
|
|
44
|
+
observation was caused by hooks not being loaded
|
|
45
|
+
at all, not by the `stop` event itself. See
|
|
46
|
+
install instructions in the README for the
|
|
47
|
+
documented user-level `~/.cursor/hooks.json`
|
|
48
|
+
install path — plugin-level hooks declared in
|
|
49
|
+
.cursor-plugin/plugin.json are NOT loaded by
|
|
50
|
+
current Cursor versions.)
|
|
51
|
+
|
|
52
|
+
To enable verbose tracing during development, set the env var
|
|
53
|
+
TRTC_HOOK_DEBUG_LOG=/path/to/file.log; see _dbg() below. The default is
|
|
54
|
+
silent (no log file written) so production installs don't accumulate logs.
|
|
55
|
+
"""
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import json
|
|
59
|
+
import os
|
|
60
|
+
import subprocess
|
|
61
|
+
import sys
|
|
62
|
+
from pathlib import Path
|
|
63
|
+
|
|
64
|
+
ADAPTER_DIR = Path(__file__).resolve().parent
|
|
65
|
+
PLUGIN_ROOT = ADAPTER_DIR.parent # plugin install root
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Optional debug logging — only writes when TRTC_HOOK_DEBUG_LOG is set to a
|
|
69
|
+
# path. Useful for diagnosing "why didn't my hook fire" without modifying
|
|
70
|
+
# the script.
|
|
71
|
+
def _dbg(msg: str) -> None:
|
|
72
|
+
log_path = os.environ.get("TRTC_HOOK_DEBUG_LOG")
|
|
73
|
+
if not log_path:
|
|
74
|
+
return
|
|
75
|
+
try:
|
|
76
|
+
import datetime as _dt
|
|
77
|
+
with open(log_path, "a") as f:
|
|
78
|
+
f.write(f"[{_dt.datetime.now().isoformat(timespec='milliseconds')}] {msg}\n")
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Always-on probe log: writes one line per adapter invocation to
|
|
84
|
+
# /tmp/trtc-hook-probe.log so we can confirm which Cursor hook events
|
|
85
|
+
# actually fire end-to-end. Off by default to avoid unbounded log files
|
|
86
|
+
# in production installs; enable by setting env TRTC_HOOK_PROBE=1.
|
|
87
|
+
def _probe_log(key, payload) -> None:
|
|
88
|
+
if os.environ.get("TRTC_HOOK_PROBE") != "1":
|
|
89
|
+
return
|
|
90
|
+
try:
|
|
91
|
+
import datetime as _dt
|
|
92
|
+
ev = (payload or {}).get("hook_event_name", "?")
|
|
93
|
+
with open("/tmp/trtc-hook-probe.log", "a") as f:
|
|
94
|
+
f.write(
|
|
95
|
+
f"[{_dt.datetime.now().isoformat(timespec='milliseconds')}] "
|
|
96
|
+
f"event={ev!r} key={key!r} "
|
|
97
|
+
f"pid={os.getpid()} "
|
|
98
|
+
f"cwd={os.environ.get('CURSOR_PROJECT_DIR') or os.getcwd()!r}\n"
|
|
99
|
+
)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Dispatch table: dispatch_key -> relative script path under PLUGIN_ROOT.
|
|
105
|
+
DISPATCH = {
|
|
106
|
+
"trtc-prepare-ui": "skills/trtc/room-builder/guardrails/trtc_prepare_ui.py",
|
|
107
|
+
"gate-slice-read": "skills/trtc-topic/guardrails/gate_slice_read.py",
|
|
108
|
+
"gate-slice-write": "skills/trtc-topic/guardrails/gate_slice_write.py",
|
|
109
|
+
"verify-ui-post-write": "skills/trtc/room-builder/guardrails/verify_ui_post_write.sh",
|
|
110
|
+
"verify-slice-must": "skills/trtc-apply/guardrails/verify_slice_must_rules.py",
|
|
111
|
+
"stop-apply-evidence": "skills/trtc-topic/guardrails/stop_require_apply_evidence.py",
|
|
112
|
+
"trtc-verify-ui": "skills/trtc/room-builder/guardrails/trtc_verify_ui.py",
|
|
113
|
+
"verify-apply-project": "skills/trtc-apply/guardrails/verify_apply_project.py",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Dispatch keys whose underlying scripts read JSON from stdin.
|
|
117
|
+
# Other keys are CLI-only and will receive empty stdin.
|
|
118
|
+
STDIN_KEYS = {
|
|
119
|
+
"gate-slice-read",
|
|
120
|
+
"gate-slice-write",
|
|
121
|
+
"verify-ui-post-write",
|
|
122
|
+
"verify-slice-must",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _silent_allow() -> None:
|
|
127
|
+
"""Fail-open: never block the user because the adapter itself failed."""
|
|
128
|
+
_dbg(" EXIT=0 (silent allow)")
|
|
129
|
+
sys.exit(0)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _read_cursor_payload() -> dict:
|
|
133
|
+
raw = sys.stdin.read()
|
|
134
|
+
if not raw or not raw.strip():
|
|
135
|
+
return {}
|
|
136
|
+
try:
|
|
137
|
+
data = json.loads(raw)
|
|
138
|
+
return data if isinstance(data, dict) else {}
|
|
139
|
+
except (ValueError, TypeError):
|
|
140
|
+
return {}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _translate_payload(key: str, cursor: dict) -> dict:
|
|
144
|
+
"""Map Cursor's per-event payload to the {tool_name, tool_input} envelope
|
|
145
|
+
the existing guardrails expect.
|
|
146
|
+
|
|
147
|
+
The existing scripts read either:
|
|
148
|
+
- {"tool_name": "Read", "tool_input": {"file_path": "..."}}
|
|
149
|
+
- {"tool_name": "Write|Edit", "tool_input": {"file_path": "..."}}
|
|
150
|
+
"""
|
|
151
|
+
if key == "gate-slice-read":
|
|
152
|
+
# Cursor's beforeReadFile payload: {file_path, content, attachments, ...}
|
|
153
|
+
file_path = cursor.get("file_path") or cursor.get("filePath")
|
|
154
|
+
return {"tool_name": "Read", "tool_input": {"file_path": file_path}}
|
|
155
|
+
|
|
156
|
+
if key == "gate-slice-write":
|
|
157
|
+
# Cursor's preToolUse payload: {tool_name, tool_input, tool_use_id, cwd, ...}
|
|
158
|
+
# Cursor only emits "Write" (no separate "Edit"); existing script
|
|
159
|
+
# accepts both. Pass through unchanged when tool_name is Write/Edit;
|
|
160
|
+
# silent allow for unrelated tools.
|
|
161
|
+
tool_name = cursor.get("tool_name") or cursor.get("toolName")
|
|
162
|
+
if tool_name not in ("Write", "Edit"):
|
|
163
|
+
return {} # caller will silent-allow
|
|
164
|
+
tool_input = cursor.get("tool_input") or cursor.get("toolInput") or {}
|
|
165
|
+
return {"tool_name": tool_name, "tool_input": tool_input}
|
|
166
|
+
|
|
167
|
+
if key in ("verify-ui-post-write", "verify-slice-must"):
|
|
168
|
+
# Cursor's afterFileEdit payload: {file_path, edits}
|
|
169
|
+
file_path = cursor.get("file_path") or cursor.get("filePath")
|
|
170
|
+
return {"tool_name": "Edit", "tool_input": {"file_path": file_path}}
|
|
171
|
+
|
|
172
|
+
# CLI-only events (sessionStart, afterAgentResponse) get empty stdin —
|
|
173
|
+
# they read state from session files and env vars, not stdin.
|
|
174
|
+
return {}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _emit_cursor_deny(message: str) -> None:
|
|
178
|
+
"""Cursor deny protocol for events that honor blocking (preToolUse,
|
|
179
|
+
beforeReadFile, afterFileEdit): JSON on stdout + exit 2.
|
|
180
|
+
|
|
181
|
+
Fields populated:
|
|
182
|
+
- permission: "deny" — tells Cursor this action should be blocked.
|
|
183
|
+
- user_message: surfaced in Cursor's UI on events that support it.
|
|
184
|
+
- agent_message: shown to the agent so it can self-correct on hooks
|
|
185
|
+
that run before the agent's loop ends (preToolUse, beforeReadFile).
|
|
186
|
+
|
|
187
|
+
NOTE: do not call this for the `stop` event. Cursor docs confirm only
|
|
188
|
+
`followup_message` is honored on stop; use _emit_cursor_followup()
|
|
189
|
+
instead so the agent gets re-prompted to fix the issue.
|
|
190
|
+
"""
|
|
191
|
+
_dbg(f" EXIT=2 (deny) message={message[:160]!r}")
|
|
192
|
+
msg = message or "Blocked by TRTC guardrail."
|
|
193
|
+
payload = {
|
|
194
|
+
"permission": "deny",
|
|
195
|
+
"user_message": msg,
|
|
196
|
+
"agent_message": msg,
|
|
197
|
+
}
|
|
198
|
+
sys.stdout.write(json.dumps(payload))
|
|
199
|
+
sys.stdout.flush()
|
|
200
|
+
sys.exit(2)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _emit_cursor_followup(message: str) -> None:
|
|
204
|
+
"""Cursor stop-event protocol: emit {"followup_message": ...} on stdout
|
|
205
|
+
and exit 0. Cursor will automatically submit the message as the next
|
|
206
|
+
user turn, giving the agent a chance to self-correct.
|
|
207
|
+
|
|
208
|
+
Per Cursor docs, the stop event only honors `followup_message`; other
|
|
209
|
+
output fields (permission, user_message, agent_message) are accepted by
|
|
210
|
+
the schema but not enforced by callers. Auto-resubmission is capped by
|
|
211
|
+
`loop_limit` (default 5) so a guardrail that keeps failing won't loop
|
|
212
|
+
forever.
|
|
213
|
+
|
|
214
|
+
Exit 0 (not 2): we want Cursor to resubmit, not block. Exit 2 on stop
|
|
215
|
+
just halts with no chance for the agent to fix the problem.
|
|
216
|
+
"""
|
|
217
|
+
msg = (message or "TRTC guardrail failed; please review and fix.").strip()
|
|
218
|
+
# Cursor caps message length on some events; keep this generous but bounded
|
|
219
|
+
# so we don't ship a 50KB stderr trace as the next user message.
|
|
220
|
+
if len(msg) > 4000:
|
|
221
|
+
msg = msg[:4000] + "\n…(truncated)"
|
|
222
|
+
_dbg(f" EXIT=0 (followup) message={msg[:160]!r}")
|
|
223
|
+
payload = {"followup_message": msg}
|
|
224
|
+
sys.stdout.write(json.dumps(payload))
|
|
225
|
+
sys.stdout.flush()
|
|
226
|
+
sys.exit(0)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def main(argv: list[str]) -> None:
|
|
230
|
+
key = argv[1] if len(argv) >= 2 else None
|
|
231
|
+
_dbg(
|
|
232
|
+
f"ENTER key={key!r} "
|
|
233
|
+
f"cursor_project_dir={os.environ.get('CURSOR_PROJECT_DIR', '?')!r}"
|
|
234
|
+
)
|
|
235
|
+
if not key:
|
|
236
|
+
_silent_allow()
|
|
237
|
+
|
|
238
|
+
# Read stdin first so debug logs see Cursor's hook_event_name even when
|
|
239
|
+
# the dispatch key is unknown or the script is missing.
|
|
240
|
+
cursor_payload = _read_cursor_payload()
|
|
241
|
+
_probe_log(key, cursor_payload)
|
|
242
|
+
_dbg(
|
|
243
|
+
f" hook_event_name={cursor_payload.get('hook_event_name', '?')!r} "
|
|
244
|
+
f"keys={sorted(cursor_payload.keys()) if cursor_payload else []}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
rel_script = DISPATCH.get(key)
|
|
248
|
+
if not rel_script:
|
|
249
|
+
# Unknown dispatch key — never block on adapter misconfiguration.
|
|
250
|
+
_silent_allow()
|
|
251
|
+
|
|
252
|
+
script = PLUGIN_ROOT / rel_script
|
|
253
|
+
if not script.exists():
|
|
254
|
+
# Script missing (e.g., skill not installed). Same convention the
|
|
255
|
+
# existing hooks.json uses ("[ ! -f X ] || python3 X").
|
|
256
|
+
_silent_allow()
|
|
257
|
+
|
|
258
|
+
claude_payload = _translate_payload(key, cursor_payload)
|
|
259
|
+
|
|
260
|
+
# gate-slice-write returns {} when tool isn't Write/Edit -> silent allow.
|
|
261
|
+
if key == "gate-slice-write" and not claude_payload:
|
|
262
|
+
_silent_allow()
|
|
263
|
+
|
|
264
|
+
# Build env: surface Cursor's project dir as CLAUDE_PROJECT_DIR so
|
|
265
|
+
# existing scripts that key off CLAUDE_PROJECT_DIR keep working.
|
|
266
|
+
env = dict(os.environ)
|
|
267
|
+
cursor_project_dir = env.get("CURSOR_PROJECT_DIR") or cursor_payload.get("cwd")
|
|
268
|
+
if cursor_project_dir and not env.get("CLAUDE_PROJECT_DIR"):
|
|
269
|
+
env["CLAUDE_PROJECT_DIR"] = cursor_project_dir
|
|
270
|
+
env["CLAUDE_PLUGIN_ROOT"] = str(PLUGIN_ROOT)
|
|
271
|
+
|
|
272
|
+
runner = ["bash", str(script)] if script.suffix == ".sh" else ["python3", str(script)]
|
|
273
|
+
stdin_data = json.dumps(claude_payload) if key in STDIN_KEYS else ""
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
proc = subprocess.run(
|
|
277
|
+
runner,
|
|
278
|
+
input=stdin_data,
|
|
279
|
+
text=True,
|
|
280
|
+
env=env,
|
|
281
|
+
capture_output=True,
|
|
282
|
+
timeout=30,
|
|
283
|
+
)
|
|
284
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
285
|
+
_silent_allow()
|
|
286
|
+
|
|
287
|
+
# Forward whatever the inner script wrote.
|
|
288
|
+
if proc.stdout:
|
|
289
|
+
sys.stderr.write(proc.stdout) # inner stdout -> stderr so we don't
|
|
290
|
+
# corrupt our Cursor JSON channel
|
|
291
|
+
if proc.stderr:
|
|
292
|
+
sys.stderr.write(proc.stderr)
|
|
293
|
+
|
|
294
|
+
rc = proc.returncode
|
|
295
|
+
_dbg(f" inner_rc={rc}")
|
|
296
|
+
if rc == 0:
|
|
297
|
+
sys.exit(0)
|
|
298
|
+
if rc in (1, 2):
|
|
299
|
+
# Translate fail/block into Cursor's response. Use stderr content as
|
|
300
|
+
# the agent-facing message so the existing scripts' human-readable
|
|
301
|
+
# explanations reach the user.
|
|
302
|
+
msg = (proc.stderr or proc.stdout or "").strip()
|
|
303
|
+
# On the `stop` event Cursor honors only `followup_message` (which
|
|
304
|
+
# auto-resubmits a corrective user turn). On preToolUse /
|
|
305
|
+
# beforeReadFile / afterFileEdit, the deny envelope is enforced.
|
|
306
|
+
if cursor_payload.get("hook_event_name") == "stop":
|
|
307
|
+
_emit_cursor_followup(msg)
|
|
308
|
+
_emit_cursor_deny(msg)
|
|
309
|
+
|
|
310
|
+
# Unknown exit code — fail open to mimic Claude Code's permissive default.
|
|
311
|
+
_silent_allow()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
main(sys.argv)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"sessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"command": "python3 $HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py trtc-prepare-ui"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"beforeReadFile": [
|
|
10
|
+
{
|
|
11
|
+
"command": "python3 $HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py gate-slice-read"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"preToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"command": "python3 $HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py gate-slice-write"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"afterFileEdit": [
|
|
20
|
+
{
|
|
21
|
+
"command": "python3 $HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py verify-ui-post-write"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"command": "python3 $HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py verify-slice-must"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"stop": [
|
|
28
|
+
{
|
|
29
|
+
"command": "python3 $HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py stop-apply-evidence"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"command": "python3 $HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py trtc-verify-ui"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"command": "python3 $HOME/.cursor/plugins/local/trtc-agent-skills/hooks/cursor-adapter.py verify-apply-project"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc/room-builder/guardrails/trtc_prepare_ui.py\" ] || python3 \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc/room-builder/guardrails/trtc_prepare_ui.py\""
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"PreToolUse": [
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Read",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-topic/guardrails/gate_slice_read.py\" ] || python3 \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-topic/guardrails/gate_slice_read.py\""
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"matcher": "Write|Edit",
|
|
25
|
+
"hooks": [
|
|
26
|
+
{
|
|
27
|
+
"type": "command",
|
|
28
|
+
"command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-topic/guardrails/gate_slice_write.py\" ] || python3 \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-topic/guardrails/gate_slice_write.py\""
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"PostToolUse": [
|
|
34
|
+
{
|
|
35
|
+
"matcher": "Write|Edit",
|
|
36
|
+
"hooks": [
|
|
37
|
+
{
|
|
38
|
+
"type": "command",
|
|
39
|
+
"command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc/room-builder/guardrails/verify_ui_post_write.sh\" ] || bash \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc/room-builder/guardrails/verify_ui_post_write.sh\""
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-apply/guardrails/verify_slice_must_rules.py\" ] || python3 \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-apply/guardrails/verify_slice_must_rules.py\""
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"Stop": [
|
|
49
|
+
{
|
|
50
|
+
"hooks": [
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-topic/guardrails/stop_require_apply_evidence.py\" ] || python3 \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-topic/guardrails/stop_require_apply_evidence.py\""
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc/room-builder/guardrails/trtc_verify_ui.py\" ] || python3 \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc/room-builder/guardrails/trtc_verify_ui.py\""
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"type": "command",
|
|
61
|
+
"command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-apply/guardrails/verify_apply_project.py\" ] || python3 \"${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}/skills/trtc-apply/guardrails/verify_apply_project.py\""
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -331,6 +331,14 @@ slices:
|
|
|
331
331
|
description: 官方 RoomKit 集成模式下 conference 对象的完整 API 签名、枚举定义、调用时序和代码示例(ui_mode=official-roomkit 专用)
|
|
332
332
|
status: active
|
|
333
333
|
|
|
334
|
+
- id: conference/integration-audit
|
|
335
|
+
name: Conference Web 集成审计
|
|
336
|
+
tags: [audit, checklist, meta, integration-review, roomkit, online-classroom]
|
|
337
|
+
platforms: [web]
|
|
338
|
+
file: slices/conference/web/integration-audit.md
|
|
339
|
+
description: 横切对照官方 Conference Web 接入流程的审计清单;Path B intent F 使用,不进 scenario 集成队列
|
|
340
|
+
status: active
|
|
341
|
+
|
|
334
342
|
# ============================================================
|
|
335
343
|
# Scenarios — 场景组合索引
|
|
336
344
|
# ============================================================
|
|
@@ -236,12 +236,15 @@ slices:
|
|
|
236
236
|
```
|
|
237
237
|
我帮你定位到「通用会议」场景。会议骨架(登录、会前预览、房间生命周期、成员列表、画面布局、设备控制、网络质量)会默认集成。
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
你的需求里我识别到要这些增强能力:{命中的可选模块中文名}
|
|
240
240
|
|
|
241
|
-
|
|
241
|
+
以上无误吗?
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
3a. **确认优先(`target_features` 已明确时)**:若第 1 步命中的可选模块非空且映射清晰,用 `AskUserQuestion` 单次确认:(1) 确认,按此集成 → 直接写 `confirmed_plan`;(2) 需要调整 → 再走下方多选。不要把用户已在 prompt 里点名的能力再让用户选一遍。
|
|
245
|
+
|
|
246
|
+
3b. **多选(仅当 `target_features` 为空/模糊,或用户选「需要调整」)**:对「可选模块」做 `multiSelect: true` 多选,预勾选第 1 步命中的项。一次超过 4 项时分组。每组末尾含「以上都不需要」(`none`)。
|
|
247
|
+
|
|
245
248
|
4. **写 `confirmed_plan`**:`confirmed_plan = 必装骨架 + 用户最终选中的可选模块`(去重,保持上面声明的顺序)。这是 `init_slice_queue.py` 的唯一输入,后续 A2-Q1.5 业务决策、slice 循环、apply 都以它为准。
|
|
246
249
|
5. **不要**把未选中的可选模块写进 `confirmed_plan`,也不要在生成代码时"顺手"加它们的按钮 / composable。
|
|
247
250
|
6. `enhancement_level` 仅为兼容旧字段:全部可选模块都选 → `complete`,否则 → `minimal`。真正的范围以 `confirmed_plan` 为准。
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: conference/integration-audit
|
|
3
|
+
name: Conference Web 集成审计
|
|
4
|
+
product: conference
|
|
5
|
+
platform: web
|
|
6
|
+
tags: [audit, checklist, meta, integration-review, roomkit, online-classroom]
|
|
7
|
+
platforms: [web]
|
|
8
|
+
related:
|
|
9
|
+
- conference/login-auth
|
|
10
|
+
- conference/prejoin-check
|
|
11
|
+
- conference/room-lifecycle
|
|
12
|
+
- conference/room-schedule
|
|
13
|
+
- conference/device-control
|
|
14
|
+
- conference/official-roomkit-api
|
|
15
|
+
related_scenario: general-conference
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Conference Web 集成审计
|
|
19
|
+
|
|
20
|
+
> **Meta slice** — 横切对照清单,供 Path B intent F(集成审计)使用。
|
|
21
|
+
> **不要**加入 scenario 的 `slices` / `confirmed_plan`,**不要**走 topic 集成队列或 apply 门禁。
|
|
22
|
+
|
|
23
|
+
> 当用户问「检查业务流程遗漏」「对照官方接入流程」「在线课堂/会议流程是否正常」时使用本 slice。
|
|
24
|
+
> 输出形态:**固定 checklist + slice 引用**;**不要**输出 code review、优缺点列表或「改进建议」段落。
|
|
25
|
+
|
|
26
|
+
## 如何使用
|
|
27
|
+
|
|
28
|
+
1. 从 session 读取 `product` / `platform` / `ui_mode` / `scenario`(若有)。
|
|
29
|
+
2. 仅 `(product, platform) == (conference, web)` 时走完整清单;其他组合说明当前 skill 集成 fix 仅覆盖 Conference Web,清单仍可作为对照参考。
|
|
30
|
+
3. 按角色(学生 / 老师 / 通用)裁剪章节;用户未说明角色时输出「通用骨架 + 在线课堂附录」。
|
|
31
|
+
4. 每项标注:**应有** / **常见遗漏** / **对应 slice**。
|
|
32
|
+
5. 用户贴了代码时,只核对清单项是否 wired up,不做风格点评。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 1. 通用骨架(所有 Conference Web 项目)
|
|
37
|
+
|
|
38
|
+
| # | 检查项 | 常见遗漏 | 参考 slice |
|
|
39
|
+
|---|--------|----------|------------|
|
|
40
|
+
| 1.1 | `conference.login()` / `useLoginState().login()` 先于任何 room/device/chat 调用 | 未登录直接 join | `conference/login-auth` |
|
|
41
|
+
| 1.2 | 登录参数含 `scene: 5001`(skill 生成代码) | 缺 scene | `conference/login-auth` |
|
|
42
|
+
| 1.3 | 登录成功后 `setSelfInfo({ userName, avatarUrl })` | 列表只显示 userId | `conference/login-auth` |
|
|
43
|
+
| 1.4 | UserSig:生产由后端签发;本地可用 config 自动签名(`local-dev`)或控制台粘贴(`console`) | userId 与 userSig 不匹配 | `conference/login-auth` |
|
|
44
|
+
| 1.5 | 监听 `LoginEvent.onLoginExpired` / `onKickedOffline` 并收口 | 只打日志 | `conference/login-auth` |
|
|
45
|
+
| 1.6 | 监听 `RoomEvent.USER_SIG_EXPIRED` / `KICKED_OUT` / `ROOM_DISMISS` | 被动退出 UI 假在线 | `conference/room-lifecycle` |
|
|
46
|
+
| 1.7 | 页面在 **HTTPS** 或 **localhost** 下调试 | 生产 HTTP 无媒体权限 | `conference/device-control` |
|
|
47
|
+
| 1.8 | iframe 嵌入时宿主页 `allow="camera; microphone; display-capture; fullscreen"` | iframe 内黑屏/无权限 | `conference/device-control` |
|
|
48
|
+
| 1.9 | 刷新/关页时 `leaveRoom()` 或等价清理 | 幽灵参会者 | `conference/room-lifecycle` |
|
|
49
|
+
| 1.10 | 房主 `endRoom()` vs 成员 `leaveRoom()` 分支正确 | 老师误用 leave 导致房间未结束 | `conference/room-lifecycle` |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. 会前设备检测
|
|
54
|
+
|
|
55
|
+
| # | 检查项 | 常见遗漏 | 参考 slice |
|
|
56
|
+
|---|--------|----------|------------|
|
|
57
|
+
| 2.1 | 会前用 `startCameraTest` / `stopCameraTest`,**不用** `openLocalCamera` 做预检 | getUserMedia 与 SDK 双轨冲突 | `conference/prejoin-check` |
|
|
58
|
+
| 2.2 | 预检页设备开关用**本地 ref**,不读 `cameraStatus` / `microphoneStatus` | 预检与会中状态串线 | `conference/prejoin-check` |
|
|
59
|
+
| 2.3 | 入会前 `stopCameraTest()` 释放采集 | 进房后设备占用 | `conference/prejoin-check` |
|
|
60
|
+
| 2.4 | 轻量 `getUserMedia` 仅作**权限门禁**;完整预览走 Atomicx 测试 API | 两套检测职责重叠 | `conference/prejoin-check` |
|
|
61
|
+
| 2.5 | 进房后麦克风:先 `openLocalMicrophone()` + 默认 `muteMicrophone()`;摄像头:`open/closeLocalCamera` | 开麦延迟、关摄语义错误 | `conference/device-control` |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 3. 房间生命周期
|
|
66
|
+
|
|
67
|
+
| # | 检查项 | 常见遗漏 | 参考 slice |
|
|
68
|
+
|---|--------|----------|------------|
|
|
69
|
+
| 3.1 | 即时会议:发起人 `createAndJoinRoom`,他人 `joinRoom` | roomId 来源混乱 | `conference/room-lifecycle` |
|
|
70
|
+
| 3.2 | 预约会议:`scheduleRoom` 成功后,到点仍须 `joinRoom`(预约≠已入会) | 预约后直接认为在会中 | `conference/room-schedule` |
|
|
71
|
+
| 3.3 | `joinRoom` 失败区分:未登录 / 房间不存在 / 密码错误 / 已结束 | 统一提示「进房失败」 | `conference/room-lifecycle` |
|
|
72
|
+
| 3.4 | 被动退出后清理 `currentRoom`、聊天、布局、widget 注册 | 再次入会残留上一场状态 | `conference/room-lifecycle` |
|
|
73
|
+
| 3.5 | `passive_exit_target`:被踢/解散后跳转大厅或登录(业务决策) | 停留在空白会中页 | `conference/room-lifecycle` |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 4. 官方 RoomKit 模式(`ui_mode = official-roomkit`)
|
|
78
|
+
|
|
79
|
+
| # | 检查项 | 常见遗漏 | 参考 slice |
|
|
80
|
+
|---|--------|----------|------------|
|
|
81
|
+
| 4.1 | 根节点 `UIKitProvider` 包裹 `PreConferenceView` / `ConferenceMainView(H5)` | Provider 缺失 | `conference/official-roomkit-api` |
|
|
82
|
+
| 4.2 | `setWidgetVisible` / `registerWidget` / `onWill` 在 **login 之后、join 之前** | 按钮闪烁、拦截失效 | `conference/official-roomkit-api` |
|
|
83
|
+
| 4.3 | `createAndJoinRoom` / `joinRoom` 成功后立即 `setFeatureConfig({ shareLink })` | 分享链接 roomId 不对 | `conference/official-roomkit-api` |
|
|
84
|
+
| 4.4 | 收集 `registerWidget` / `onWill` 返回的 cleanup;在 `ROOM_LEAVE` 与 `ROOM_DISMISS` 都执行 | 重复注册、泄漏 | `conference/official-roomkit-api` |
|
|
85
|
+
| 4.5 | UI 显隐用 `setWidgetVisible`,**不用** CSS/DOM hack | 升级 SDK 即碎 | `conference/official-roomkit-api` |
|
|
86
|
+
| 4.6 | lockfile 中 `@tencentcloud/roomkit-web-vue3 >= 5.4.3` | 定制 API 不可用 | `conference/official-roomkit-api` |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 5. 在线课堂角色附录
|
|
91
|
+
|
|
92
|
+
### 学生链路
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
登录 → setSelfInfo → 预约列表(room-schedule) → 会前检测(prejoin-check)
|
|
96
|
+
→ joinRoom → 会中(默认禁麦策略)→ 主动 leaveRoom / 被动 ROOM_DISMISS
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| 常见遗漏 | 说明 |
|
|
100
|
+
|----------|------|
|
|
101
|
+
| 预约后直接进房未做设备检测 | 应用 prejoin-check 页或等价流程 |
|
|
102
|
+
| 被房主结束会议无提示 | 监听 `RoomEvent.ROOM_DISMISS` |
|
|
103
|
+
| 与「只 join 不 create」的 roomid_origin 不一致 | session 业务决策应对齐 |
|
|
104
|
+
|
|
105
|
+
### 老师链路
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
登录 → setSelfInfo → createAndJoinRoom → 会中开启 AI 字幕(ai-tools) / 录制(控制台+套餐)
|
|
109
|
+
→ endRoom(非 leaveRoom)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| 常见遗漏 | 说明 |
|
|
113
|
+
|----------|------|
|
|
114
|
+
| AI 字幕非房主也调 `startRealtimeTranscriber` | 仅房主应启动 |
|
|
115
|
+
| 结束会议用 `leaveRoom` 而非 `endRoom` | 其他成员仍留在已结束房间上下文 |
|
|
116
|
+
| 录制能力未在控制台开通 | 预制 UI 按钮灰显或启动失败 |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 6. 高频运行时问题速查(审计时顺带提及)
|
|
121
|
+
|
|
122
|
+
| 现象 / 错误码 | 结论 | 下一步 |
|
|
123
|
+
|---------------|------|--------|
|
|
124
|
+
| **6206** / UserSig expired | 鉴权失败 | 后端刷新 UserSig;监听 `USER_SIG_EXPIRED` |
|
|
125
|
+
| **100211** take-seat not enabled | 非麦位房间内部 seat 同步 | Standard 会议通常**可忽略**;确认未调用上麦/连麦 API |
|
|
126
|
+
| **100006** only open to live room | 在 Standard 房间调了 Live 接口 | 检查 roomType;隐藏 Live 工具栏 widget |
|
|
127
|
+
| 双设备检测冲突 | 职责未分层 | 见 §2 |
|
|
128
|
+
| iframe 无画面 | 缺 allow 权限 | 见 §1.8 |
|
|
129
|
+
| 刷新后幽灵在线 | 未 beforeunload leave | 见 §1.9 |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 7. 推荐输出模板(给 AI)
|
|
134
|
+
|
|
135
|
+
```markdown
|
|
136
|
+
## Conference Web 集成对照结果
|
|
137
|
+
|
|
138
|
+
**场景**:{scenario 或「通用会议」} · **UI 模式**:{official-roomkit | headless | 未知}
|
|
139
|
+
|
|
140
|
+
### 已覆盖(对照官方流程)
|
|
141
|
+
- …
|
|
142
|
+
|
|
143
|
+
### 可能遗漏(按优先级)
|
|
144
|
+
1. … → 参考 `conference/xxx`
|
|
145
|
+
2. …
|
|
146
|
+
|
|
147
|
+
### 环境/权限门禁
|
|
148
|
+
- HTTPS / iframe / UserSig:…
|
|
149
|
+
|
|
150
|
+
### 官方文档
|
|
151
|
+
- [Web RoomKit 快速接入](https://cloud.tencent.com/document/product/647/81962)
|
|
152
|
+
- [设备检测](https://cloud.tencent.com/document/product/647/126939)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**禁止**使用的标题:Critical Review Checklist、改进建议、✅正确 vs ❌错误 对比表(作为主结构)。
|
|
@@ -14,7 +14,8 @@ business_decisions:
|
|
|
14
14
|
tier: blocking
|
|
15
15
|
question: "您的项目当前处于哪个阶段?这决定鉴权凭证(UserSig)的生成方式。"
|
|
16
16
|
options:
|
|
17
|
-
- { label: "本地开发
|
|
17
|
+
- { label: "本地开发 —— config 配置 SDKAppID+SecretKey,登录时按 UserID 自动签名(推荐,最快)", value: "local-dev" }
|
|
18
|
+
- { label: "本地开发 / 调试 —— 到控制台生成 UserSig,复制粘贴到登录页(不配置 SecretKey)", value: "console" }
|
|
18
19
|
- { label: "生产环境(上线)—— 由您的后端接口签发,安全合规(推荐)", value: "backend" }
|
|
19
20
|
- key: userid_strategy
|
|
20
21
|
tier: blocking
|