diff --git a/.env.example b/.env.example index f30adbf..2095c1c 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# ── 运行模式 / adapter 档位(见 references/ADAPTER.md)── +# ── 运行模式 / adapter 档位(见 development/ADAPTER.md)── OPENCLAW_TEST_TARGET=mock # mock | simulator_rpa | real_api | real_rpa # ── 目标系统 ── @@ -12,7 +12,7 @@ OPENCLAW_BROWSER_HEADLESS=0 # 0=有头(默认) 1=无头(CI) OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认) # ── 存证 ── -OPENCLAW_RECORD_VIDEO=1 +OPENCLAW_RECORD_VIDEO=0 # 0=默认不录屏;1=启用录屏+字幕+旁白+背景音+MP4 OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认) # ── 节流 / 超时 ── diff --git a/development/CONFIG.md b/development/CONFIG.md index 61bc5e0..b4cb3f7 100644 --- a/development/CONFIG.md +++ b/development/CONFIG.md @@ -31,7 +31,7 @@ OPENCLAW_BROWSER_HEADLESS=0 # 0=有头(默认) 1=无头(CI) OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认) # ── 录屏与失败存证 ── -OPENCLAW_RECORD_VIDEO=1 # 1=录屏+字幕+旁白+背景音+MP4(RPA 默认开,见 RPA.md) +OPENCLAW_RECORD_VIDEO=0 # 0=默认不录屏;1=启用录屏+字幕+旁白+背景音+MP4(见 RPA.md) OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认) # ── 节流 / 超时 ── @@ -42,6 +42,13 @@ HUMAN_WAIT_TIMEOUT=180 # 滑块/验证码/2FA 等人工超时( **业务专属配置必须带技能前缀**(如 `DEMO_XXX`、`MY_SKILL_XXX`),**不要污染全局命名空间**(禁止 `SCRAPE_1688_*`、`RECEIVE_ORDER_*` 等跨技能前缀写入模板)。 +### 录屏开关(`OPENCLAW_RECORD_VIDEO`) + +- **模板默认 `OPENCLAW_RECORD_VIDEO=0`**,降低本地开发 / 调试成本(不启 ffmpeg、不生成 MP4)。 +- 需要**生产留证、合规记录、客户现场复现或排障**时,可在用户 `.env` 或进程环境变量中设为 `OPENCLAW_RECORD_VIDEO=1`。 +- **默认关闭不代表可以删除录屏接入**:RPA / 长任务代码仍须保留 `RpaVideoSession`、`video.add_step` 与 video artifact 写入 `result_summary`(见 `RPA.md` §5.3)。 +- **已落盘的用户 `.env` 不会被模板整体覆盖**;老用户保留原值,技能升级时仅通过 `merge_missing_env_keys` **追加** `.env.example` 中新增的 key。 + ## 🚫 红线:敏感信息不进 `.env` `.env` 只放**非敏感运行参数**(模式、地址、开关、超时、账号标识)。密码、密钥、动态口令、token—— diff --git a/development/LOGGING.md b/development/LOGGING.md index 8cbdd9c..11ba71b 100644 --- a/development/LOGGING.md +++ b/development/LOGGING.md @@ -88,11 +88,13 @@ OpenClaw 技能常在客户电脑上异步执行,且大量依赖 RPA、浏览 **来源**:`RpaVideoSession`、`rpa-artifacts/` 目录 +- video artifact 是**标准可启用能力**,**不保证默认每次生成 MP4**(模板默认 `OPENCLAW_RECORD_VIDEO=0`)。 +- **`OPENCLAW_RECORD_VIDEO=0`**:不生成最终录屏,但 RPA / 长任务仍须保留 `RpaVideoSession`、`video.add_step` 与 Activity 进度同步。 +- **`OPENCLAW_RECORD_VIDEO=1`**:`result_summary` 应包含 `video_path`、`raw_video`、`video_log`、`video_warnings` 等字段;CLI 可打印录屏路径与诊断。 - 用户可见的中文步骤(「打开登录页」「点击提交」)。 -- 录屏 MP4、`video_log`(ffmpeg 诊断)。 -- 失败截图:`{数据目录}/rpa-artifacts/{batch_id}/...` +- 失败截图:`{数据目录}/rpa-artifacts/{batch_id}/...`(受 `OPENCLAW_ARTIFACTS_ON_FAILURE` 控制,默认开) -详见 [`RPA.md`](RPA.md) §失败存证与录屏标准。 +详见 [`RPA.md`](RPA.md) §5 存证与录屏规范。 --- @@ -254,7 +256,7 @@ def cmd_run(target=None, input_id=None): 3. **`python scripts/main.py log-get `** — 单条 JSON,含 `result_summary`(video 路径等)。 4. **统一日志文件** — `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(grep `trace_id`、`task_failed`)。 5. **Run Journal** — `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`(宿主 UI 进度来源)。 -6. **RPA artifacts** — `rpa-artifacts/` 截图、录屏 MP4、`video_log`。 +6. **RPA artifacts** — `rpa-artifacts/` 失败截图;若 `OPENCLAW_RECORD_VIDEO=1` 还有录屏 MP4、`video_log`。未开启录屏时可能只有截图或日志,需要完整录屏请在用户 `.env` 中设置 `OPENCLAW_RECORD_VIDEO=1`。 长时间无 stdout **不代表卡死**;先看 Run Journal / Activity,再看统一日志。 diff --git a/development/POLICY_MATRIX.md b/development/POLICY_MATRIX.md index 72a7f93..530bbc9 100644 --- a/development/POLICY_MATRIX.md +++ b/development/POLICY_MATRIX.md @@ -15,6 +15,7 @@ | POLICY-TESTING-002 | `tests/integration/` 下 Python 测试文件若存在,默认应为 `.sample`,不得进入默认套件 | development/TESTING.md §7 真实联调测试的安全约束、§8 新 skill 的最小测试清单 | hard | 检查 `tests/integration/*.py` 无 `.sample` 后缀 | `tests/test_development_policy_guard.py::TestPolicyTesting002` | | POLICY-RPA-001 | 不得 import/use account-manager 内部 `rpa_helpers` / `inject_account_manager_scripts_path` / `get_account_credential` | development/ADAPTER.md §兄弟依赖;development/RPA.md §1.7 | hard | 扫描 `scripts/**/*.py` 禁止模式 | `tests/test_development_policy_guard.py::TestPolicyRpa001`(另见 `tests/test_no_rpa_helpers_import.py`) | | POLICY-RPA-002 | RPA 交付代码不得直接执行 ffmpeg 拼 MP4;应使用 `RpaVideoSession`(允许 `ffmpeg_path` 等诊断字段) | development/RPA.md §5.3 录屏成片标准 | hard | 扫描 `scripts/**/*.py` 禁止 `subprocess` / `os.system` / `os.popen` 直接调用 ffmpeg(`task_run_support.py` 保留豁免) | `tests/test_development_policy_guard.py::TestPolicyRpa002` | +| POLICY-RPA-003 | 即使默认 `OPENCLAW_RECORD_VIDEO=0`,RPA / 长任务模板也必须保留录屏能力:`RpaVideoSession`、`video.add_step`、video artifact merge 到 `result_summary` | development/RPA.md §5.3;development/LOGGING.md;development/CONFIG.md | hard | 扫描 `scripts/service/*.py`(service 层整体须含上述接入) | `tests/test_development_policy_guard.py::TestPolicyRpa003` | | POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束;development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` | | POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM,无 `U+FEFF` | development/DEVELOPMENT.md §3.4;development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` | | POLICY-DOCS-001 | 本矩阵存在且包含上述全部 `policy_id` | 本次规范化约定 | hard | 解析 `development/POLICY_MATRIX.md` | `tests/test_development_policy_guard.py::TestPolicyDocs001` | diff --git a/development/RPA.md b/development/RPA.md index c29258d..86509a1 100644 --- a/development/RPA.md +++ b/development/RPA.md @@ -283,13 +283,16 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层 ### 5.3 录屏成片标准 -- RPA skill 默认 `OPENCLAW_RECORD_VIDEO=1`。 +- **skill-template 默认 `OPENCLAW_RECORD_VIDEO=0`**(开发调试更快,不启 ffmpeg)。 +- **`OPENCLAW_RECORD_VIDEO=1` 时启用录屏**:字幕、旁白、背景音、MP4 成片由 platform-kit 统一处理。 - 使用 platform-kit 的 **`RpaVideoSession`**;**skill 不要自行合成视频**(不要自己调 ffmpeg 拼 MP4)。 -- `OPENCLAW_RECORD_VIDEO=0` 时 session 无副作用(不启 ffmpeg、不写字幕文件)。 +- **默认关闭 ≠ 可以删除录屏接入**:RPA / 长任务 **service 层**(`scripts/service/*.py`)**必须保留** `RpaVideoSession`、`video.add_step`、video artifact merge 到 `result_summary`(见 `POLICY-RPA-003`)。 +- `OPENCLAW_RECORD_VIDEO=0` 时 session **无副作用**(不启 ffmpeg、不写字幕文件、不生成最终 MP4),但 **`RpaVideoSession.add_step` 仍可同步 Activity 进度**。 +- 生产留证 / 合规 / 客户现场复现时,由用户在 `.env` 或进程环境变量中开启 `OPENCLAW_RECORD_VIDEO=1`。 - **ffmpeg 是唯一录屏器**(Windows:`gdigrab` + `desktop`)。 -- **最终视频**:`{skill_data_dir}/videos/{skill_slug}_{yyyyMMdd_HHmmss}_{batch_id}.mp4` +- **最终视频**(`OPENCLAW_RECORD_VIDEO=1` 时):`{skill_data_dir}/videos/{skill_slug}_{yyyyMMdd_HHmmss}_{batch_id}.mp4` - **中间产物**:`rpa-artifacts/{batch_id}/capture.mp4`、`subtitles/`、`logs/` 等。 -- 任务完成后 CLI / `result_summary` 应包含:`video_path`、`raw_video`、`video_log`、`video_warnings`、`music_path`、`voiceover_path`、`audio_warnings`(见 `scripts/service/task_run_support.py`)。 +- 任务完成后 CLI / `result_summary` 应包含 video 字段(开启录屏时有路径;关闭时仍保留 `video` 结构与诊断占位):`video_path`、`raw_video`、`video_log`、`video_warnings`、`music_path`、`voiceover_path`、`audio_warnings`(见 `scripts/service/task_run_support.py`)。 模板最小示范见 `scripts/service/task_service.py` 的 `_run_template_demo()`。 diff --git a/development/TESTING.md b/development/TESTING.md index b3bac24..8bc5a03 100644 --- a/development/TESTING.md +++ b/development/TESTING.md @@ -236,10 +236,11 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t ## 12. RPA / video 测试标准 -- `RpaVideoSession` 调用**不跑真实 ffmpeg**:单测中使用 `unittest.mock` patch session 或设 `OPENCLAW_RECORD_VIDEO=0`。 +- 模板 `.env.example` 默认 **`OPENCLAW_RECORD_VIDEO=0`**;默认必跑测试**不应启动真实 ffmpeg**(patch session 或显式设 `OPENCLAW_RECORD_VIDEO=0`)。 +- **但测试必须守护录屏接入能力**(`POLICY-RPA-003` 扫描 `scripts/service/*.py`):`RpaVideoSession` 被构造、`video.add_step` 被调用、video artifact 结构 merge 进 `result_summary`(`video` / `video_path` 等字段),与默认是否录屏无关。 - 断言 `title` / `closing_title` 是**中文**业务文案。 -- 断言 video artifact 会进入 `result_summary`(`video_path`、`raw_video`、`video_log` 等)。 - 断言 step 文案贴近用户动作,不是技术日志(如「准备执行示例任务」而非「enter cmd_run」)。 +- 若需测试**真实录屏**(ffmpeg 成片),必须显式设 `OPENCLAW_RECORD_VIDEO=1`,并放在 `tests/integration/` 或手动触发场景,**不得**进入默认 unittest 套件。 参考 `tests/test_video_service.py`。 diff --git a/references/CLI.md b/references/CLI.md index e5a2215..b35925d 100644 --- a/references/CLI.md +++ b/references/CLI.md @@ -22,8 +22,9 @@ python {baseDir}/scripts/main.py version 2. `python {baseDir}/scripts/main.py log-get ` 3. 统一日志文件 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(含 `trace_id`、`task_failed` 等) 4. Run Journal `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl` - 5. RPA artifacts / `video_log` / 失败截图(见 `development/RPA.md`) -- **任务完成后**若有 video artifact,CLI 应打印录屏路径、录屏日志、视频/音频诊断(见 `task_run_support._print_video_summary`)。 + 5. RPA artifacts / `video_log` / 失败截图(见 `development/RPA.md`);未开启录屏时可能只有截图或日志 +- **录屏默认关闭**:模板 `.env.example` 为 `OPENCLAW_RECORD_VIDEO=0`;若未开启录屏,任务**可能不会**打印录屏路径。 +- 需要录屏时在用户 `.env` 或进程环境变量中设置 **`OPENCLAW_RECORD_VIDEO=1`**;开启后任务完成时 CLI 应打印录屏路径、录屏日志、视频/音频诊断(见 `task_run_support._print_video_summary`),且 `result_summary` 含 video 字段。 ## config-path(配置路径) diff --git a/tests/test_development_policy_guard.py b/tests/test_development_policy_guard.py index 07825b5..29516aa 100644 --- a/tests/test_development_policy_guard.py +++ b/tests/test_development_policy_guard.py @@ -32,6 +32,7 @@ POLICY_IDS = ( "POLICY-TESTING-002", "POLICY-RPA-001", "POLICY-RPA-002", + "POLICY-RPA-003", "POLICY-PACKAGING-001", "POLICY-PACKAGING-002", "POLICY-DOCS-001", @@ -109,6 +110,10 @@ RPA_002_FFMPEG_PATTERNS = ( RPA_002_EXEMPT_REL = "scripts/service/task_run_support.py" +RPA_003_SOURCE = ( + "development/RPA.md §5.3; development/TESTING.md §12; development/POLICY_MATRIX.md" +) + LOGGING_001_SOURCE = "development/LOGGING.md; development/RUNTIME.md" LOGGING_002_SOURCE = "development/LOGGING.md; development/DEVELOPMENT.md" LOGGING_003_SOURCE = "development/LOGGING.md" @@ -470,6 +475,30 @@ class TestPolicyRpa002(unittest.TestCase): ) +class TestPolicyRpa003(unittest.TestCase): + def test_service_layer_retains_video_session_integration(self) -> None: + skill_root = get_skill_root() + service_files = _walk_files(skill_root, "scripts/service", suffix=".py") + self.assertTrue(service_files, msg="scripts/service/*.py not found") + combined = "\n".join(_read_text(path) for path in service_files) + missing: list[str] = [] + if "RpaVideoSession" not in combined: + missing.append("RpaVideoSession") + if "video.add_step" not in combined and ".add_step(" not in combined: + missing.append("video.add_step or .add_step(") + if "merge_video_into_result_summary" not in combined: + missing.append("merge_video_into_result_summary") + self.assertEqual( + missing, + [], + msg=_policy_msg( + "POLICY-RPA-003", + RPA_003_SOURCE, + "scripts/service/*.py missing: " + ", ".join(missing), + ), + ) + + def _scan_sensitive_logging_assignments(skill_root: str) -> list[str]: offenders: list[str] = [] for path in _walk_files(skill_root, "scripts", suffix=".py"): diff --git a/tests/test_video_service.py b/tests/test_video_service.py index 384af5e..faab747 100644 --- a/tests/test_video_service.py +++ b/tests/test_video_service.py @@ -15,6 +15,46 @@ from _support import IsolatedDataRoot, get_skill_root from jiangchang_skill_core import config +def _active_env_value(text: str, key: str) -> str | None: + """Return the active (non-comment) KEY=value line from .env-style text.""" + prefix = f"{key}=" + for line in text.splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + active = stripped.split("#", 1)[0].strip() + if active.startswith(prefix): + return active + return None + + +def _combined_service_source(skill_root: str | None = None) -> str: + """Merge scripts/service/*.py source (excludes __pycache__).""" + root = skill_root or get_skill_root() + service_dir = os.path.join(root, "scripts", "service") + parts: list[str] = [] + for dirpath, dirnames, filenames in os.walk(service_dir): + dirnames[:] = [d for d in dirnames if d != "__pycache__"] + for name in sorted(filenames): + if not name.endswith(".py"): + continue + path = os.path.join(dirpath, name) + with open(path, encoding="utf-8") as f: + parts.append(f.read()) + return "\n".join(parts) + + +def _missing_video_integration_tokens(service_text: str) -> list[str]: + missing: list[str] = [] + if "RpaVideoSession" not in service_text: + missing.append("RpaVideoSession") + if "video.add_step" not in service_text and ".add_step(" not in service_text: + missing.append("video.add_step or .add_step(") + if "merge_video_into_result_summary" not in service_text: + missing.append("merge_video_into_result_summary") + return missing + + class TestEnvExampleVideoDefaults(unittest.TestCase): def test_env_example_contains_required_keys(self) -> None: path = os.path.join(get_skill_root(), ".env.example") @@ -22,13 +62,45 @@ class TestEnvExampleVideoDefaults(unittest.TestCase): text = f.read() for key in ( "OPENCLAW_" + "TEST_TARGET=mock", - "OPENCLAW_RECORD_VIDEO=1", + "OPENCLAW_RECORD_VIDEO=0", "OPENCLAW_ARTIFACTS_ON_FAILURE=1", "OPENCLAW_BROWSER_HEADLESS=0", "OPENCLAW_PLAYWRIGHT_STEALTH=1", ): self.assertIn(key, text, msg=f"missing {key!r} in .env.example") + def test_env_example_default_off_but_template_keeps_video_integration(self) -> None: + path = os.path.join(get_skill_root(), ".env.example") + with open(path, encoding="utf-8") as f: + text = f.read() + self.assertEqual( + _active_env_value(text, "OPENCLAW_RECORD_VIDEO"), + "OPENCLAW_RECORD_VIDEO=0", + ) + + service_text = _combined_service_source() + missing = _missing_video_integration_tokens(service_text) + self.assertEqual( + missing, + [], + msg=( + "default OPENCLAW_RECORD_VIDEO=0 does not allow removing video integration; " + f"scripts/service/*.py missing: {', '.join(missing)}" + ), + ) + + def test_env_example_allows_record_video_one_in_comments_only(self) -> None: + sample = "\n".join( + [ + "# example: OPENCLAW_RECORD_VIDEO=1 for production", + "OPENCLAW_RECORD_VIDEO=0", + ] + ) + self.assertEqual( + _active_env_value(sample, "OPENCLAW_RECORD_VIDEO"), + "OPENCLAW_RECORD_VIDEO=0", + ) + class TestPrintVideoSummary(unittest.TestCase): def test_cli_prints_video_path_when_enabled(self) -> None: