fix: harden skill template contracts and scaffolding
All checks were successful
技能自动化发布 / release (push) Successful in 4s
All checks were successful
技能自动化发布 / release (push) Successful in 4s
Align scaffold slug replacement, Action manifest strict boundaries, metadata prune API, field permission validation, and task_logs readonly semantics with documented template contracts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -47,20 +47,20 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns (
|
||||
TASK_LOGS_TABLE_METADATA = {
|
||||
"table_name": TASK_LOGS_TABLE,
|
||||
"display_name": "任务日志",
|
||||
"description": "记录技能任务的执行状态和结果",
|
||||
"description": "记录技能任务的执行状态和结果(系统自动写入,宿主数据管理只读展示)",
|
||||
"visible": 1,
|
||||
"readonly": 0,
|
||||
"readonly": 1,
|
||||
}
|
||||
|
||||
TASK_LOGS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
||||
{"column_name": "id", "display_name": "编号", "description": "任务日志唯一编号", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "task_type", "display_name": "任务类型", "description": "任务的业务类型", "visible": 1, "searchable": 1, "editable": 1},
|
||||
{"column_name": "target_id", "display_name": "目标编号", "description": "任务目标对象编号", "visible": 1, "searchable": 1, "editable": 1},
|
||||
{"column_name": "input_id", "display_name": "输入编号", "description": "输入对象编号", "visible": 1, "searchable": 1, "editable": 1},
|
||||
{"column_name": "input_title", "display_name": "输入标题", "description": "输入对象标题", "visible": 1, "searchable": 1, "editable": 1},
|
||||
{"column_name": "status", "display_name": "状态", "description": "任务当前状态", "visible": 1, "searchable": 1, "editable": 1},
|
||||
{"column_name": "error_msg", "display_name": "错误信息", "description": "任务失败时的错误说明", "visible": 1, "searchable": 1, "editable": 1},
|
||||
{"column_name": "result_summary", "display_name": "结果摘要", "description": "任务执行结果摘要", "visible": 1, "searchable": 1, "editable": 1},
|
||||
{"column_name": "task_type", "display_name": "任务类型", "description": "任务的业务类型", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "target_id", "display_name": "目标编号", "description": "任务目标对象编号", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "input_id", "display_name": "输入编号", "description": "输入对象编号", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "input_title", "display_name": "输入标题", "description": "输入对象标题", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "status", "display_name": "状态", "description": "任务当前状态", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "error_msg", "display_name": "错误信息", "description": "任务失败时的错误说明", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "result_summary", "display_name": "结果摘要", "description": "任务执行结果摘要", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{
|
||||
"column_name": "created_at",
|
||||
"display_name": "创建时间",
|
||||
@@ -157,27 +157,22 @@ def seed_task_logs_display_metadata(cursor) -> None:
|
||||
visible=int(meta["visible"]),
|
||||
readonly=int(meta["readonly"]),
|
||||
)
|
||||
for column in TASK_LOGS_COLUMN_METADATA:
|
||||
upsert_column_metadata(
|
||||
cursor,
|
||||
table_name=TASK_LOGS_TABLE,
|
||||
column_name=str(column["column_name"]),
|
||||
display_name=str(column["display_name"]),
|
||||
description=str(column["description"]) if column.get("description") else None,
|
||||
visible=int(column["visible"]),
|
||||
searchable=int(column["searchable"]),
|
||||
editable=int(column["editable"]),
|
||||
display_type=str(column["display_type"]) if column.get("display_type") else None,
|
||||
options_json=str(column["options_json"]) if column.get("options_json") else None,
|
||||
)
|
||||
seed_column_metadata_batch(
|
||||
cursor,
|
||||
table_name=TASK_LOGS_TABLE,
|
||||
columns=TASK_LOGS_COLUMN_METADATA,
|
||||
valid_column_names=[str(column["column_name"]) for column in TASK_LOGS_COLUMN_METADATA],
|
||||
)
|
||||
|
||||
|
||||
def seed_column_metadata_batch(
|
||||
cursor,
|
||||
*,
|
||||
table_name: str,
|
||||
columns: tuple[dict[str, object], ...],
|
||||
columns: Iterable[dict[str, object]],
|
||||
valid_column_names: Iterable[str] | None = None,
|
||||
) -> None:
|
||||
"""批量 upsert 字段元数据;传入 valid_column_names 时会在 upsert 后清理陈旧字段元数据。"""
|
||||
for column in columns:
|
||||
upsert_column_metadata(
|
||||
cursor,
|
||||
@@ -191,6 +186,8 @@ def seed_column_metadata_batch(
|
||||
display_type=str(column["display_type"]) if column.get("display_type") else None,
|
||||
options_json=str(column["options_json"]) if column.get("options_json") else None,
|
||||
)
|
||||
if valid_column_names is not None:
|
||||
prune_stale_column_metadata(cursor, table_name, valid_column_names)
|
||||
|
||||
|
||||
def prune_stale_column_metadata(cursor, table_name: str, valid_column_names: Iterable[str]) -> None:
|
||||
|
||||
@@ -161,6 +161,33 @@ def _table_column_info(cursor, table_name: str) -> dict[str, dict[str, object]]:
|
||||
return info
|
||||
|
||||
|
||||
def _table_column_xinfo(cursor, table_name: str) -> dict[str, dict[str, object]]:
|
||||
"""PRAGMA table_xinfo 可识别 generated/hidden 字段(SQLite 3.31+)。"""
|
||||
cursor.execute(f"PRAGMA table_xinfo({_quote_identifier(table_name)})")
|
||||
info: dict[str, dict[str, object]] = {}
|
||||
for row in cursor.fetchall():
|
||||
hidden = int(row[6] or 0) if len(row) > 6 else 0
|
||||
info[str(row[1])] = {
|
||||
"cid": int(row[0]),
|
||||
"name": str(row[1]),
|
||||
"type": str(row[2] or ""),
|
||||
"notnull": int(row[3] or 0),
|
||||
"dflt_value": row[4],
|
||||
"pk": int(row[5] or 0),
|
||||
"hidden": hidden,
|
||||
}
|
||||
return info
|
||||
|
||||
|
||||
def _is_generated_column(xinfo: dict[str, object]) -> bool:
|
||||
hidden = int(xinfo.get("hidden") or 0)
|
||||
return hidden in (2, 3)
|
||||
|
||||
|
||||
def _is_blob_column(column_info: dict[str, object]) -> bool:
|
||||
return "BLOB" in str(column_info.get("type") or "").upper()
|
||||
|
||||
|
||||
def _default_uses_unixepoch(default_value: object) -> bool:
|
||||
if default_value is None:
|
||||
return False
|
||||
@@ -202,6 +229,7 @@ def validate_column_permissions(
|
||||
warnings: list[str] = []
|
||||
ordered = physical_columns or list_physical_column_names(cursor, table_name)
|
||||
column_info = _table_column_info(cursor, table_name)
|
||||
column_xinfo = _table_column_xinfo(cursor, table_name)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
@@ -216,13 +244,13 @@ def validate_column_permissions(
|
||||
for row in cursor.fetchall()
|
||||
}
|
||||
|
||||
for column_name in ordered:
|
||||
meta = meta_by_column.get(column_name)
|
||||
if not meta:
|
||||
for column_name, meta in meta_by_column.items():
|
||||
physical = column_info.get(column_name) or {}
|
||||
xinfo = column_xinfo.get(column_name) or physical
|
||||
if not physical and column_name not in column_xinfo:
|
||||
continue
|
||||
|
||||
physical = column_info.get(column_name) or {}
|
||||
pk = int(physical.get("pk") or 0)
|
||||
pk = int(physical.get("pk") or xinfo.get("pk") or 0)
|
||||
editable = meta["editable"]
|
||||
if pk and editable is not None and int(editable) != 0:
|
||||
errors.append(f"{table_name}.{column_name} 是主键,editable 应为 0")
|
||||
@@ -230,6 +258,12 @@ def validate_column_permissions(
|
||||
if column_name == "id" and editable is not None and int(editable) != 0:
|
||||
errors.append(f"{table_name}.id 的 editable 应为 0(自增主键)")
|
||||
|
||||
if _is_generated_column(xinfo) and editable is not None and int(editable) != 0:
|
||||
errors.append(f"{table_name}.{column_name} 是 generated 字段,editable 应为 0")
|
||||
|
||||
if _is_blob_column(physical or xinfo) and editable is not None and int(editable) != 0:
|
||||
errors.append(f"{table_name}.{column_name} 是 BLOB 字段,editable 应为 0")
|
||||
|
||||
opt_errors, _ = validate_options_json(meta.get("options_json"))
|
||||
for item in opt_errors:
|
||||
errors.append(f"{table_name}.{column_name}: {item}")
|
||||
|
||||
Reference in New Issue
Block a user