Release v1.0.113: document default_sort may use hidden columns
All checks were successful
技能自动化发布 / release (push) Successful in 13s
All checks were successful
技能自动化发布 / release (push) Successful in 13s
This commit is contained in:
@@ -9,6 +9,11 @@
|
||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||
|
||||
## 1.0.113
|
||||
|
||||
- 固化数据管理契约:`visible=0` 只藏界面;`default_sort_json` 仍可引用隐藏的非敏感物理列(含主键)
|
||||
- 元数据校验:排序列必须真实存在;引用隐藏列仅提示、不报错
|
||||
|
||||
## 1.0.112
|
||||
|
||||
- 新技能的名字不必再以平台或 `default` 结尾。没有抖音、小红书、亚马逊这类外部平台时,用动词加名词即可
|
||||
|
||||
2
SKILL.md
2
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.112
|
||||
version: 1.0.113
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
|
||||
@@ -776,6 +776,7 @@ HINT: <next step>
|
||||
- 依赖或宣传表顶栏「更多操作」溢出菜单(宿主已取消该能力)。
|
||||
- 把「表格行数据默认排序」与按钮顺序混淆——行排序见 [`SCHEMA.md`](SCHEMA.md) 的 `default_sort_json`。
|
||||
- 把「默认展示列」与 `visible=0` 硬隐藏混淆——默认展示列见 [`SCHEMA.md`](SCHEMA.md) 的 `default_visible_columns_json`。
|
||||
- 误以为 `visible=0` 的列不能出现在 `default_sort_json`——隐藏只影响展示/筛选/搜索;ORDER BY 仍可用非敏感物理列(含隐藏主键)。
|
||||
- 新增 `row-context-menu` 等 placement,或要求技能再维护一份与 `row` 平行的菜单清单。
|
||||
|
||||
### Action 类型 × placements(常见示例,不是限制)
|
||||
|
||||
@@ -180,11 +180,36 @@ JSON 数组(可多字段,按顺序):
|
||||
|
||||
规则:
|
||||
|
||||
- `column` 必须是该表真实列名;敏感列会被宿主忽略
|
||||
- `column` 必须是该表**真实物理列名**(存在于 SQLite 表);不存在则宿主忽略该段或技能校验报错
|
||||
- **可以**引用 `visible=0` 的列(含隐藏的主键 `id`)。`visible=0` 只表示界面不展示 / 不进默认 SELECT·筛选·搜索;**不表示不能 ORDER BY**
|
||||
- 敏感列(宿主标为 sensitive)不得用于排序;技能侧勿把密钥类字段写进 `default_sort_json`
|
||||
- `direction` 仅 `asc` / `desc`(缺省按 `desc`)
|
||||
- 用户在 UI 上手动改排序后,以用户选择为准;声明只影响未选择时的默认
|
||||
- 通过 `upsert_table_metadata(..., default_sort_json=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||||
|
||||
与 `visible` 的分工(勿再踩坑):
|
||||
|
||||
| 能力 | `visible=0` 是否仍可用 |
|
||||
|------|------------------------|
|
||||
| 表格列展示 / 「列」面板 | 否(硬隐藏) |
|
||||
| 默认 SELECT、列筛选、顶栏搜索 | 否 |
|
||||
| **`default_sort_json` / ORDER BY** | **是**(非敏感列) |
|
||||
| 行主键定位(`$row.$pk` 等) | 是(主键即使隐藏仍应可排序、可定位;宿主查询层保留) |
|
||||
|
||||
示例(编号硬隐藏,但仍按编号排序——合法):
|
||||
|
||||
```python
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name="period_rule_overrides",
|
||||
display_name="本月默认金额",
|
||||
readonly=1,
|
||||
default_sort_json='[{"column":"period","direction":"desc"},{"column":"id","direction":"asc"}]',
|
||||
)
|
||||
# id 列元数据可见性:
|
||||
# upsert_column_metadata(..., column_name="id", visible=0, ...)
|
||||
```
|
||||
|
||||
示例(消息时间线):
|
||||
|
||||
```python
|
||||
@@ -277,6 +302,8 @@ upsert_column_metadata(
|
||||
- 只写英文物理名而不写中文 `display_name`
|
||||
- 让元数据引用不存在的表
|
||||
- 用 `visible=0` 假装「默认不展示但用户还能勾出来」(那是硬隐藏,勾不回来)
|
||||
- 误以为「列 `visible=0` 就不能写进 `default_sort_json`」——排序与展示已拆开;隐藏编号/主键仍可排序(须列真实存在且非敏感)
|
||||
- 把不存在的列名写进 `default_sort_json`(技能校验应报错;宿主也会忽略无效段)
|
||||
|
||||
### `_jiangchang_columns` 字段级标准
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ def upsert_table_metadata(
|
||||
default_sort_json: 宿主数据管理默认排序,JSON 数组,例如
|
||||
'[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]'
|
||||
为空则宿主回退到 updated_at → created_at → 主键。
|
||||
排序列须为真实物理列;**允许**引用 visible=0 的非敏感列(隐藏≠不能 ORDER BY)。
|
||||
|
||||
default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如
|
||||
'["stat_type","period","stage"]'
|
||||
|
||||
@@ -279,6 +279,95 @@ def validate_filter_search_metadata(
|
||||
return [], warnings
|
||||
|
||||
|
||||
def parse_default_sort_json(raw: object) -> tuple[list[dict[str, str]], list[str]]:
|
||||
"""解析 default_sort_json;返回 (specs, errors)。specs 项含 column / direction。"""
|
||||
errors: list[str] = []
|
||||
if raw is None or str(raw).strip() == "":
|
||||
return [], []
|
||||
text = str(raw).strip()
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return [], ["default_sort_json 不是合法 JSON"]
|
||||
if not isinstance(parsed, list):
|
||||
return [], ["default_sort_json 必须是 JSON 数组"]
|
||||
out: list[dict[str, str]] = []
|
||||
for index, item in enumerate(parsed):
|
||||
if not isinstance(item, dict):
|
||||
errors.append(f"default_sort_json[{index}] 必须是对象")
|
||||
continue
|
||||
column = str(
|
||||
item.get("column")
|
||||
or item.get("columnName")
|
||||
or item.get("column_name")
|
||||
or ""
|
||||
).strip()
|
||||
direction_raw = str(item.get("direction") or "desc").strip().lower()
|
||||
if not column:
|
||||
errors.append(f"default_sort_json[{index}] 缺少 column")
|
||||
continue
|
||||
if direction_raw not in {"asc", "desc"}:
|
||||
errors.append(f"default_sort_json[{index}].direction 只能是 asc 或 desc")
|
||||
continue
|
||||
out.append({"column": column, "direction": direction_raw})
|
||||
return out, errors
|
||||
|
||||
|
||||
def validate_default_sort_metadata(
|
||||
cursor,
|
||||
table_name: str,
|
||||
*,
|
||||
physical_columns: list[str] | None = None,
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""校验表级 default_sort_json:列须真实存在;允许 visible=0;不存在则报错。
|
||||
|
||||
契约:visible=0 只藏界面,ORDER BY 仍可用非敏感物理列(含隐藏主键)。
|
||||
"""
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
table_cols = {
|
||||
str(row[1]) for row in cursor.execute("PRAGMA table_info(_jiangchang_tables)").fetchall()
|
||||
}
|
||||
if "default_sort_json" not in table_cols:
|
||||
return [], []
|
||||
cursor.execute(
|
||||
"SELECT default_sort_json FROM _jiangchang_tables WHERE table_name = ?",
|
||||
(table_name,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return [], []
|
||||
specs, parse_errors = parse_default_sort_json(row[0])
|
||||
for msg in parse_errors:
|
||||
errors.append(f"{table_name}: {msg}")
|
||||
if parse_errors:
|
||||
return errors, warnings
|
||||
|
||||
physical = set(physical_columns or list_physical_column_names(cursor, table_name))
|
||||
for spec in specs:
|
||||
column = spec["column"]
|
||||
if column not in physical:
|
||||
errors.append(
|
||||
f"{table_name}: default_sort_json 引用了不存在的列 {column!r}"
|
||||
)
|
||||
continue
|
||||
# visible=0 合法:不报错。仅提示契约,避免作者误删排序列。
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT visible FROM _jiangchang_columns
|
||||
WHERE table_name = ? AND column_name = ?
|
||||
""",
|
||||
(table_name, column),
|
||||
)
|
||||
meta = cursor.fetchone()
|
||||
if meta is not None and meta[0] is not None and int(meta[0]) == 0:
|
||||
warnings.append(
|
||||
f"{table_name}.{column}: default_sort_json 引用了 visible=0 列"
|
||||
"(合法:隐藏列仍可 ORDER BY;见 references/SCHEMA.md)"
|
||||
)
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def validate_column_permissions(
|
||||
cursor,
|
||||
table_name: str,
|
||||
@@ -558,6 +647,14 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
|
||||
_, filter_warnings = validate_filter_search_metadata(cur, table_name)
|
||||
report.warnings.extend(filter_warnings)
|
||||
|
||||
sort_errors, sort_warnings = validate_default_sort_metadata(
|
||||
cur,
|
||||
table_name,
|
||||
physical_columns=physical_columns,
|
||||
)
|
||||
report.errors.extend(sort_errors)
|
||||
report.warnings.extend(sort_warnings)
|
||||
|
||||
cur.execute("SELECT table_name, column_name FROM _jiangchang_columns")
|
||||
for table_name, column_name in cur.fetchall():
|
||||
if table_name in METADATA_TABLES:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.112"
|
||||
SKILL_VERSION = "1.0.113"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
@@ -595,6 +595,53 @@ metadata:
|
||||
self.assertIn("readonly = 1", text)
|
||||
self.assertIn("seed_column_metadata_batch", text)
|
||||
self.assertIn("valid_column_names", text)
|
||||
self.assertIn("可以**引用 `visible=0` 的列", text)
|
||||
self.assertIn("default_sort_json` / ORDER BY", text)
|
||||
|
||||
def test_default_sort_json_allows_hidden_column_and_rejects_missing(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
from db.display_metadata import (
|
||||
init_display_metadata_tables,
|
||||
seed_column_metadata_batch,
|
||||
upsert_table_metadata,
|
||||
)
|
||||
from db.display_metadata_validator import validate_default_sort_metadata
|
||||
|
||||
cur = conn.cursor()
|
||||
init_display_metadata_tables(cur)
|
||||
cur.execute(
|
||||
"CREATE TABLE demo_sort (id INTEGER PRIMARY KEY, title TEXT NOT NULL)"
|
||||
)
|
||||
upsert_table_metadata(
|
||||
cur,
|
||||
table_name="demo_sort",
|
||||
display_name="排序示例",
|
||||
default_sort_json='[{"column":"title","direction":"asc"},{"column":"id","direction":"desc"}]',
|
||||
)
|
||||
seed_column_metadata_batch(
|
||||
cur,
|
||||
table_name="demo_sort",
|
||||
columns=(
|
||||
{"column_name": "id", "display_name": "编号", "visible": 0, "editable": 0},
|
||||
{"column_name": "title", "display_name": "标题", "visible": 1, "editable": 1},
|
||||
),
|
||||
valid_column_names=["id", "title"],
|
||||
)
|
||||
errors, warnings = validate_default_sort_metadata(cur, "demo_sort")
|
||||
self.assertEqual(errors, [])
|
||||
self.assertTrue(any("visible=0" in w and "id" in w for w in warnings))
|
||||
|
||||
upsert_table_metadata(
|
||||
cur,
|
||||
table_name="demo_sort",
|
||||
display_name="排序示例",
|
||||
default_sort_json='[{"column":"missing_col","direction":"asc"}]',
|
||||
)
|
||||
errors, _ = validate_default_sort_metadata(cur, "demo_sort")
|
||||
self.assertTrue(any("missing_col" in e for e in errors))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_default_visible_columns_json_upsert_and_migrate(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
|
||||
Reference in New Issue
Block a user