diff --git a/.env.example b/.env.example index 2e39ea3..1dcb1e7 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ # 运行模式:决定本技能用模拟数据还是真实浏览器/接口去执行 OPENCLAW_TEST_TARGET=real_rpa # 可选:mock=本地模拟,simulator_rpa=仿真站点,real_api=真实接口,real_rpa=真实浏览器;模板默认真实浏览器;本地单测/CI 请用 mock +# @required # 目标网站地址:技能要访问的网站或服务地址 TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4294d8d..e019a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.115 + +- 沉淀匠厂配置管理必填项契约:`.env.example` 在行头上方用独立一行 `# @required` 声明;宿主显示红色 `*` 并在保存时校验非空 +- 模板示例已为 `TARGET_BASE_URL` 加上示范;新增 `POLICY-CONFIG-005` 门禁错误用法 + ## 1.0.114 - 技能根目录增加 `sync.ps1`,和 `release.ps1` 并排。在本机把明文同步到已安装的技能目录,用来在宿主里验收 diff --git a/SKILL.md b/SKILL.md index 5c5821b..94f9580 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.114 +version: 1.0.115 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/development/CONFIG.md b/development/CONFIG.md index dc64ed2..25e6645 100644 --- a/development/CONFIG.md +++ b/development/CONFIG.md @@ -26,6 +26,45 @@ **不要**依赖 `# ── 分组 ──` 装饰线给整组做唯一说明:宿主只会把行头挂到**紧挨着的下一个活跃 key**,后面的 key 不会自动继承。分组可用空行区分;**逐 key 自带行头(+ 行尾)才是硬要求**。 +### 必填项:`# @required`(匠厂配置管理) + +匠厂宿主「数据管理 → 配置管理」支持把部分配置项标为**必填**:模板在**行头注释上方**加独立一行 `# @required`,宿主会在 key 前显示红色 `*`,并在键值模式保存时拦截空值。 + +| 规则 | 说明 | +|------|------| +| **写法** | `# @required` 单独占一行,紧挨在该 key 的**中文行头**上方 | +| **语义** | 保存时该 key 的值不得为空(trim 后);模板里可以有默认值 | +| **展示** | `@required` 行本身**不展示**给用户,只用于打标 | +| **声明位置** | 只在仓库 `.env.example` 声明;用户数据目录 `.env` 里的 `@required` 不算 | + +正例: + +```ini +# @required +# 目标网站地址:技能要访问的网站或服务地址 +TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改 + +# 默认登录账号:仅填写非敏感的账号标识(如工号/登录名) +DEFAULT_LOGIN_ID= # 留空=按平台自动选用;本项未标 @required,宿主不强制 +``` + +反例(禁止): + +```ini +# 目标网站地址(必填) +TARGET_BASE_URL= # @required ← 禁止写在行尾 + +# @required +TARGET_BASE_URL= # ← 禁止:@required 下方缺少中文行头 + +# @required +# 共用说明 +KEY_A= +KEY_B= # ← 禁止:一个 @required 给多个 key 共用 +``` + +**机器门禁**:`POLICY-CONFIG-005` 校验 `@required` 用法;与 `POLICY-CONFIG-004`(行头/行尾中文)一并生效。 + ### 必须做到 1. 每个会进入配置管理的活跃 `KEY=` 都有完整中文**行头**。 diff --git a/development/DEVELOPMENT.md b/development/DEVELOPMENT.md index d84078c..faa60fe 100644 --- a/development/DEVELOPMENT.md +++ b/development/DEVELOPMENT.md @@ -967,6 +967,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea - [ ] 没有残留旧平台名 - [ ] `health` / `version` 可运行 - [ ] `.env.example` 注释已按 [`CONFIG.md`](CONFIG.md)「行头 = 是什么、行尾 = 怎么填」写全(面向配置管理用户;无 `见 development/...` 等开发文档指向) +- [ ] 业务必填项已在 `.env.example` 用独立一行 `# @required` 标记(见 [`CONFIG.md`](CONFIG.md) §必填项);未标则宿主不显示红色 `*` - [ ] `.gitignore` 生效,没有把 `__pycache__` 提交进去 - [ ] `release.ps1` 存在(发布前会自动跑 `python tests/run_tests.py -v`;失败不得打 tag) - [ ] `.github/workflows/release_skill.yaml` 存在 diff --git a/development/POLICY_MATRIX.md b/development/POLICY_MATRIX.md index 122874e..f8b4a59 100644 --- a/development/POLICY_MATRIX.md +++ b/development/POLICY_MATRIX.md @@ -14,6 +14,7 @@ | POLICY-CONFIG-002 | `.gitignore` 必须忽略 `.env` 与 `*.env.local` | development/CONFIG.md §红线:敏感信息不进 .env | hard | 解析 `.gitignore` 行 | `tests/test_development_policy_guard.py::TestPolicyConfig002` | | POLICY-CONFIG-003 | 业务代码不得直接 `os.environ` / `os.getenv` / `os.putenv` / `environ.get`;须经 `jiangchang_skill_core.config.get*`(允许 `main.py`、测试、config_bootstrap/runtime 边界文件) | development/CONFIG.md §配置 bootstrap;development/ADAPTER.md §档位 dispatch | hard | 扫描 `scripts/**/*.py` 并排除边界文件 | `tests/test_development_policy_guard.py::TestPolicyConfig003` | | POLICY-CONFIG-004 | `.env.example` 每个活跃 `KEY=` 必须有中文**行头**(是什么)与中文**行尾**(怎么填);注释禁止 `development/`、`*.md` 文档指向、`account-manager`、`adapter 档位` 等开发黑话 | development/CONFIG.md §.env.example 注释 = 配置管理用户文案 | hard | 解析 `.env.example` 活跃行与注释 | `tests/test_development_policy_guard.py::TestPolicyConfig004` | +| POLICY-CONFIG-005 | 若 `.env.example` 使用 `# @required`,须单独占一行且在下一行中文行头之上;禁止写在行尾;禁止无中文行头或多 key 共用 | development/CONFIG.md §必填项:# @required | hard | 解析 `.env.example` 活跃行与 `@required` 标记 | `tests/test_development_policy_guard.py::TestPolicyConfig005` | | POLICY-TESTING-001 | 默认根目录 `tests/test_*.py` 不得硬编码 `real_api` / `real_rpa` / `OPENCLAW_TEST_TARGET`(白名单仅 `test_adapter_profile_policy.py`,用于档位 helper 自测) | development/TESTING.md §默认必跑测试要做什么、§4 测试目标档位 | hard | 扫描 `tests/test_*.py` 非注释行;白名单外禁止上述文本 | `tests/test_development_policy_guard.py::TestPolicyTesting001` | | 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`) | diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 511e1e7..bd5a68a 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.114" +SKILL_VERSION = "1.0.115" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.11" diff --git a/tests/test_development_policy_guard.py b/tests/test_development_policy_guard.py index 650c841..9bcb932 100644 --- a/tests/test_development_policy_guard.py +++ b/tests/test_development_policy_guard.py @@ -30,6 +30,7 @@ POLICY_IDS = ( "POLICY-CONFIG-002", "POLICY-CONFIG-003", "POLICY-CONFIG-004", + "POLICY-CONFIG-005", "POLICY-TESTING-001", "POLICY-TESTING-002", "POLICY-RPA-001", @@ -96,8 +97,11 @@ CONFIG_ENV_FORBIDDEN_PATTERNS = ( ) CONFIG_004_SOURCE = "development/CONFIG.md §.env.example 注释 = 配置管理用户文案" +CONFIG_005_SOURCE = "development/CONFIG.md §必填项:# @required" CONFIG_004_ENV_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") CONFIG_004_CJK_RE = re.compile(r"[\u4e00-\u9fff]") +CONFIG_005_REQUIRED_MARKER_RE = re.compile(r"^@required\b", re.IGNORECASE) +CONFIG_005_INLINE_REQUIRED_RE = re.compile(r"@required\b", re.IGNORECASE) # 注释中禁止出现的开发向指向 / 黑话(大小写不敏感子串) CONFIG_004_FORBIDDEN_COMMENT_SUBSTRINGS = ( "development/", @@ -316,6 +320,34 @@ def _config_004_comment_offenders(path: str) -> list[str]: return offenders +def _is_env_required_marker(text: str) -> bool: + return bool(CONFIG_005_REQUIRED_MARKER_RE.match(text.strip())) + + +def _config_005_offenders(path: str) -> list[str]: + offenders: list[str] = [] + for lineno, key, _value, headers, inline in _iter_env_example_active_entries(path): + if inline and CONFIG_005_INLINE_REQUIRED_RE.search(inline): + offenders.append( + f".env.example:{lineno}: {key}= 禁止在行尾写 @required(须单独一行放在中文行头上方)" + ) + + required_markers = [part for part in headers if _is_env_required_marker(part)] + non_marker_headers = [part for part in headers if not _is_env_required_marker(part)] + if not required_markers: + continue + if not non_marker_headers: + offenders.append( + f".env.example:{lineno}: {key}= 使用了 # @required 但缺少中文行头" + ) + continue + if not any(CONFIG_004_CJK_RE.search(part) for part in non_marker_headers): + offenders.append( + f".env.example:{lineno}: {key}= # @required 下方须有中文行头(说明「是什么」)" + ) + return offenders + + def _rel(skill_root: str, path: str) -> str: return os.path.relpath(path, skill_root).replace("\\", "/") @@ -575,6 +607,23 @@ class TestPolicyConfig004(unittest.TestCase): ) +class TestPolicyConfig005(unittest.TestCase): + def test_env_example_required_markers_are_well_formed(self) -> None: + path = os.path.join(get_skill_root(), ".env.example") + self.assertTrue(os.path.isfile(path), msg=".env.example missing") + offenders = _config_005_offenders(path) + self.assertEqual( + offenders, + [], + msg=_policy_msg( + "POLICY-CONFIG-005", + CONFIG_005_SOURCE, + "\n".join(offenders) + or "every # @required marker needs its own Chinese header line above the key", + ), + ) + + class TestPolicyTesting001(unittest.TestCase): def test_root_tests_do_not_hardcode_real_targets(self) -> None: skill_root = get_skill_root()