Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f3aa7f8f |
@@ -20,16 +20,16 @@ metadata:
|
||||
1. 命中下方高频意图时直接使用精确骨架,不先查 Help 或产品级 Schema。
|
||||
2. 路由优先级固定为:精确 recipe / 可运行脚本 > 匹配的公开 Shortcut > 原子命令。命令已确定且参数清楚时直接执行。
|
||||
3. 参数、约束或安全语义不确定时只读 leaf Schema:`dws schema --cli-path "aitable <leaf>" --format json`;只有当前 Cobra flag 不确定时才读对应 `--help`。
|
||||
4. 复杂字段、筛选、视图、权限或工作流任务按“低频能力与 Reference”只加载相关文件。所有下表的 `references/aitable/...` 都是相对本 Skill 根目录的完整精确路径;不得省略中间的 `aitable/`。dashboard/chart、导入、导出、批量字段和附件统一只读 `references/aitable/aitable-script-recipes.md`,再运行 `scripts/aitable_ops.py`;不要预读整个 reference 目录或任何脚本源码。
|
||||
4. 复杂任务按下表只读相关 Reference;路径相对本 Skill 根目录且必须保留 `aitable/`。脚本型任务只读 `references/aitable/aitable-script-recipes.md`,再运行 `scripts/aitable_ops.py`;不预读目录或源码。
|
||||
5. 现有骨架和 reference 都无法定位能力时,才用 Runtime Shortcut Catalog 做最后发现;不得猜 `cli_path` 或 flag。
|
||||
6. Schema、Help、reference 与实际返回冲突时采用更安全的解释并报告契约漂移;`confirmation=user_required` 时先确认,再添加 `--yes`。
|
||||
7. 用户已给足名称、字段、数据和目标时,直接按依赖链完成全部步骤;不要调用 todo 工具、分步汇报或追问已明确的信息。中间返回只用于提取下一步 ID 和判断失败,完成所有请求后再统一回读并答复。bundled script 参数明确时直接运行;只有参数不明确时执行统一入口的操作级 `--help`,只有契约失败、环境异常或用户要求修改脚本时才读取源码。
|
||||
7. 输入完整时直接完成依赖链,不调用 todo、不分步汇报或重复追问;中间响应只提取 ID/错误,最后统一回读答复。脚本参数明确就执行;不明确才读操作级 `--help`,仅契约失败、环境异常或用户要求修改时读源码。
|
||||
8. 用户要求新建 Base 但未指定 Base 名时,根据业务目标生成简短描述性名称(例如仪表盘任务用“数据看板”)并继续;不要仅为可回退的容器名称追问。Base 只接受 Base flags,不得把 table `--fields` 传给 `base create`。
|
||||
|
||||
<!-- VISIBLE_SHORTCUTS_START -->
|
||||
## Shortcut 发现(按需)
|
||||
|
||||
`aitable` 当前有 29 条公开 shortcut。完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation 的子集可通过 leaf Schema 查询。高频产品根 Skill 不重复展开完整清单。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
|
||||
29 条公开 shortcut 保留在 Runtime Catalog,不在根 Skill 展开。已知意图直接走下方路由;仅参数/安全语义不确定时读 leaf Schema,Cobra flags 不确定时读 leaf Help。
|
||||
|
||||
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service aitable --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
|
||||
<!-- VISIBLE_SHORTCUTS_END -->
|
||||
@@ -62,7 +62,7 @@ metadata:
|
||||
| 更新记录 | `dws aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<id>","cells":{"<fieldId>":<值>}}]' --format json` | 先 query 拿 recordId;只传需改字段;取 `data.recordIds[]` 后回读 |
|
||||
| 删除记录 | 先 `dws aitable +record-query ...` 定位,再 `dws aitable record delete --base-id <baseId> --table-id <tableId> --record-ids <ids>` | 展示目标与影响,得到明确确认后才加 `--yes` |
|
||||
| 创建 Base / Table | `dws aitable base create --name "<名>" --format json` → `dws aitable base get --base-id <baseId> --format json`;`dws aitable table create --base-id <id> --name "<名>" --fields '[...]' --format json` → `dws aitable +table-get --base-id <id> --table-ids <tableId> --format json` | 使用创建返回的真实 ID 立即回读;创建字段时回读 `fields[]` 的名称、类型与 config;系统改名/加后缀时不得继续猜原名 |
|
||||
| 创建仪表盘 / 常用图表 | `python3 <本 Skill 绝对目录>/scripts/aitable_ops.py dashboard <baseId> "<仪表盘名>" [--chart-specs <workspace内JSON>]` | 唯一首选;完整参数与 ledger 契约只读 `references/aitable/aitable-script-recipes.md` |
|
||||
| 创建仪表盘 / 常用图表 | `python3 <本 Skill 绝对目录>/scripts/aitable_ops.py dashboard <baseId> "<仪表盘名>" [--chart-specs-file <workspace内JSON文件>]` | 唯一首选;完整参数与 ledger 契约只读 `references/aitable/aitable-script-recipes.md` |
|
||||
| 复制视图 | `dws aitable view duplicate --base-id <baseId> --table-id <tableId> --view-id <源viewId> --new-name "<新名称>" --format json` | 源 viewId 来自当前表的真实返回;不要复制数据表或创建仪表盘替代 |
|
||||
| 导入 / 导出 / 批量字段 / 附件 | 先读 `references/aitable/aitable-script-recipes.md`,再运行其中唯一的 `scripts/aitable_ops.py <operation> ...` | 不直接选择底层脚本;不读源码;保留统一入口返回的退出状态与 ledger |
|
||||
|
||||
@@ -72,7 +72,8 @@ metadata:
|
||||
|
||||
- `record create/update` 前必须获取目标字段的 `fieldId`、`type` 与 `config`;`filterUp`、`lookup` 等只读字段不可写。完整格式只在需要时读精确路径 `references/aitable/aitable-cell-value.md`。
|
||||
- 筛选和排序字段使用 `fieldId`;`--filters` 最外层是 `and|or + operands`,`--sort` 使用 `direction: asc|desc`。日期和跨表字段规则按需读精确路径 `references/aitable/aitable-filter-sort.md`。
|
||||
- `record query --all` 仍受 `--page-limit` 约束;分页中断或局部富化失败时保留已有结果,输出 completeness 与逐项失败 ledger,不把部分结果描述为全量。
|
||||
- **Complete evidence:** if an exact total/set relies on paginated output, use `--all --page-limit 0` or iterate until `nextCursor` is empty and `hasMore` is false. A complete write-result ID list is sufficient; a truncated page is not.
|
||||
- **Capability proof:** with write authorization, create minimal prerequisites, execute, and read back. Help/Schema alone is unverified.
|
||||
- 创建、更新、导入、批量建字段等写操作必须检查业务 `status`、逐项结果与返回 ID;普通写入按用户明确要求执行后回读,不能只凭退出码宣称成功。
|
||||
- 长 JSON 使用 `--records-file` / 任务文件;不得为绕过字段错误而静默丢列、改类型或删除失败项。
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ python3 <本 Skill 绝对目录>/scripts/aitable_ops.py dashboard <BASE_ID> "<
|
||||
|
||||
# 建仪表盘和常用图表
|
||||
python3 <本 Skill 绝对目录>/scripts/aitable_ops.py dashboard <BASE_ID> "<仪表盘名>" \
|
||||
--chart-specs <workspace内/charts.json>
|
||||
--chart-specs-file <workspace内/charts.json>
|
||||
```
|
||||
|
||||
统一入口的 dashboard 操作是 `dashboard create → chart create(可选)→ dashboard get` 的唯一首选
|
||||
|
||||
@@ -87,6 +87,8 @@ dws aitable field create --base-id <BASE_ID> --table-id <TABLE_ID> \
|
||||
|
||||
创建 `lookup`(关联引用)和 `filterUp`(查找引用)字段时,config 格式有严格要求:
|
||||
|
||||
当用户要求判断此能力且已授权创建资源时,不要只读 Help/Schema 后下结论:创建最小目标表及必需字段,执行一次真实字段创建并用 `field get` 回读。若没有写入授权,只能报告“接口声明支持但未实际验证”。
|
||||
|
||||
#### bidirectionalLink / unidirectionalLink(关联字段)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -33,6 +33,8 @@ Flags:
|
||||
- **被截断时**(达到 page-limit 但仍有数据):输出中包含 `"hasMore": true` 和 `"cursor": "..."` 字段,可通过 `--cursor` 从断点继续拉取
|
||||
- 适用于需要一次性获取全量数据的场景(如导出、统计、批量处理)
|
||||
|
||||
当精确计数或集合依赖分页查询时,优先使用 `--all --page-limit 0`;否则持续传回 `nextCursor`/`cursor`,直到游标为空且 `hasMore` 不为 true。写入响应返回的完整 ID 列表可直接作为该批证据;截断页、局部计数或最终链接数不能替代分页完成信号。
|
||||
|
||||
```bash
|
||||
# 默认(最多 50 页 = 5000 条)
|
||||
dws aitable record query --base-id X --table-id Y --all
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Bundled script recipes
|
||||
|
||||
Use this file only for dashboard/chart, import, export, bulk fields, or attachment workflows.
|
||||
The stable entry point is `scripts/aitable_ops.py`; do not read it or the delegated `.py` files before execution. Run `python3 <Skill绝对目录>/scripts/aitable_ops.py --help` or the operation-level `--help` only when an argument is unclear.
|
||||
Use `scripts/aitable_ops.py` as the stable entry; do not read implementation files. Use operation-level `--help` only for unclear arguments.
|
||||
|
||||
For a create-Base-then-script workflow, create the Base first and take `baseId` from its structured response. If the user did not specify the container name, choose a short descriptive name and continue instead of asking only for that name. Verify the Base with `dws aitable base get --base-id <baseId> --format json`, then pass that exact `baseId` to this entry point.
|
||||
For create-Base-then-script, create and verify the Base, then pass its returned `baseId`; choose a short name if omitted.
|
||||
|
||||
| Intent | Exact command |
|
||||
|---|---|
|
||||
| Create dashboard, optionally charts | `python3 <Skill绝对目录>/scripts/aitable_ops.py dashboard <baseId> "<dashboardName>" [--chart-specs <workspace JSON>]` |
|
||||
| Create dashboard, optionally charts | `python3 <Skill绝对目录>/scripts/aitable_ops.py dashboard <baseId> "<dashboardName>" [--chart-specs-file <workspace JSON file>]` |
|
||||
| Import CSV/XLS/XLSX as a new table | `python3 <Skill绝对目录>/scripts/aitable_ops.py import-new <baseId> <file>` |
|
||||
| Append JSON/CSV to an existing table | `python3 <Skill绝对目录>/scripts/aitable_ops.py import-records <baseId> <tableId> <file> [--batch-size 100]` |
|
||||
| Export Base/table/view | `python3 <Skill绝对目录>/scripts/aitable_ops.py export <baseId> --scope all\|table\|view [--table-id <id>] [--view-id <id>] [--output <path>]` |
|
||||
@@ -16,8 +16,8 @@ For a create-Base-then-script workflow, create the Base first and take `baseId`
|
||||
|
||||
## Contracts
|
||||
|
||||
- `dashboard` creates the dashboard/charts, chains returned IDs, performs final dashboard/chart readback, and emits `dws-skill-script-ledger/v1`. Do not add a second guessed dashboard command after it. `--chart-specs` is a JSON array; each item accepts `name`, `chart_type`, `table_id`, `measure_type`, `measure_field_id`, `dimension_field_id`, `aggregation`, and `view_id`.
|
||||
- `import-new` runs prepare → HTTPS PUT → import task and checks each business status. It is not interchangeable with `import-records`.
|
||||
- `dashboard` creates the dashboard/charts, chains returned IDs, performs final readback, and emits `dws-skill-script-ledger/v1`. `--chart-specs-file` is a workspace-local file containing one JSON array; it is not inline JSON. Each item accepts `name`, `chart_type`, `table_id`, `measure_type`, `measure_field_id`, `dimension_field_id`, `aggregation`, and `view_id`. Do not add a guessed dashboard command after it.
|
||||
- `import-new` runs prepare → secure PUT → import task and checks each business status. A returned HTTP upload URL is upgraded to HTTPS without changing host, path, or query. It is not interchangeable with `import-records`, and a deterministic failure is not a reason to expand the same flow manually.
|
||||
- `import-records` requires CSV headers to be field IDs; use JSON for typed boolean/array/object values. It checks batch results and reads back returned record IDs.
|
||||
- `export --scope table|view` requires `--table-id`; view also requires `--view-id`. The unified ledger exposes the real `taskId`, `polledTimes`, `savedPath`, and `fileSize`; treat success plus a non-empty saved file as the completion evidence, without reading source or substituting `ls` for task polling. Do not overwrite unless the user explicitly requests it.
|
||||
- `add-fields` accepts at most 15 items and reports partial failure. `upload-attachment` only returns `fileToken`; write that token to the attachment field and read the record back.
|
||||
|
||||
@@ -22,7 +22,7 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
|
||||
@@ -52,12 +52,24 @@ def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
|
||||
return None
|
||||
|
||||
|
||||
def put_file(upload_url: str, file_path: Path) -> Tuple[bool, str]:
|
||||
def normalize_upload_url(upload_url: str) -> str:
|
||||
parsed = urlparse(upload_url)
|
||||
if parsed.scheme != "https" or not parsed.hostname:
|
||||
return False, "uploadUrl must be a valid HTTPS URL"
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
||||
raise ValueError("uploadUrl must be a valid HTTP(S) URL")
|
||||
if parsed.username or parsed.password:
|
||||
raise ValueError("uploadUrl must not contain user info")
|
||||
if parsed.scheme == "http":
|
||||
parsed = parsed._replace(scheme="https")
|
||||
return urlunparse(parsed)
|
||||
|
||||
|
||||
def put_file(upload_url: str, file_path: Path) -> Tuple[bool, str]:
|
||||
try:
|
||||
secure_upload_url = normalize_upload_url(upload_url)
|
||||
except ValueError as exc:
|
||||
return False, str(exc)
|
||||
payload = file_path.read_bytes()
|
||||
req = Request(upload_url, data=payload, method="PUT")
|
||||
req = Request(secure_upload_url, data=payload, method="PUT")
|
||||
# 关键:清空 Content-Type,避免 SignatureDoesNotMatch。
|
||||
req.add_header("Content-Type", "")
|
||||
try:
|
||||
|
||||
@@ -17,7 +17,7 @@ def command_for(args: argparse.Namespace) -> list[str]:
|
||||
if args.operation == "dashboard":
|
||||
command = [
|
||||
"create_dashboard_chart.py", args.base_id, args.dashboard_name,
|
||||
*(["--chart-specs", args.chart_specs] if args.chart_specs else []),
|
||||
*(["--chart-specs-file", args.chart_specs_file] if args.chart_specs_file else []),
|
||||
]
|
||||
elif args.operation == "import-new":
|
||||
command = ["aitable_import_via_task.py", args.base_id, args.file]
|
||||
@@ -56,7 +56,10 @@ def parser() -> argparse.ArgumentParser:
|
||||
dashboard = sub.add_parser("dashboard", help="create and read back a dashboard/chart")
|
||||
dashboard.add_argument("base_id")
|
||||
dashboard.add_argument("dashboard_name")
|
||||
dashboard.add_argument("--chart-specs", help="workspace JSON array of chart specs")
|
||||
dashboard.add_argument(
|
||||
"--chart-specs-file", "--chart-specs", dest="chart_specs_file",
|
||||
help="workspace-local file containing a JSON array of chart specs",
|
||||
)
|
||||
|
||||
import_new = sub.add_parser("import-new", help="import CSV/XLS/XLSX as a new table")
|
||||
import_new.add_argument("base_id")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Examples:
|
||||
python3 create_dashboard_chart.py BASE_ID "状态分析仪表盘"
|
||||
python3 create_dashboard_chart.py BASE_ID "状态分析仪表盘" --chart-specs charts.json
|
||||
python3 create_dashboard_chart.py BASE_ID "状态分析仪表盘" --chart-specs-file charts.json
|
||||
|
||||
charts.json is a JSON array. Each item accepts:
|
||||
name, chart_type, table_id, measure_type, measure_field_id,
|
||||
@@ -193,7 +193,10 @@ def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("base_id", help="Target AI Table baseId")
|
||||
parser.add_argument("dashboard_name", help="Dashboard name")
|
||||
parser.add_argument("--chart-specs", help="Workspace-local JSON chart spec file")
|
||||
parser.add_argument(
|
||||
"--chart-specs-file", "--chart-specs", dest="chart_specs_file",
|
||||
help="Workspace-local file containing a JSON array of chart specs",
|
||||
)
|
||||
parser.add_argument("--dws", default="dws", help="dws executable")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -202,7 +205,7 @@ def main() -> int:
|
||||
if not RESOURCE_ID_PATTERN.fullmatch(base_id) or not dashboard_name:
|
||||
parser.error("base_id and dashboard_name are required and must be valid")
|
||||
try:
|
||||
specs = validate_specs(safe_json_file(args.chart_specs)) if args.chart_specs else []
|
||||
specs = validate_specs(safe_json_file(args.chart_specs_file)) if args.chart_specs_file else []
|
||||
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
parser.error(str(exc))
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ class AITableSkillScriptsTest(unittest.TestCase):
|
||||
def test_unified_ops_dispatches_without_cross_operation_arguments(self):
|
||||
cases = [
|
||||
(
|
||||
["dashboard", "base12345678", "概览", "--chart-specs", "charts.json"],
|
||||
["dashboard", "base12345678", "概览", "--chart-specs-file", "charts.json"],
|
||||
"create_dashboard_chart.py",
|
||||
["base12345678", "概览", "--chart-specs", "charts.json"],
|
||||
["base12345678", "概览", "--chart-specs-file", "charts.json"],
|
||||
),
|
||||
(
|
||||
["import-new", "base12345678", "data.csv"],
|
||||
@@ -251,7 +251,7 @@ elif args[:3] == ['aitable', 'field', 'get']:
|
||||
self.assertEqual(payload["verifiedCount"], 1)
|
||||
self.assertEqual(payload["ledger"][1]["error"], "duplicate")
|
||||
|
||||
def test_upload_helpers_reject_plain_http_urls(self):
|
||||
def test_upload_helpers_use_secure_urls(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
file_path = Path(raw) / "file.bin"
|
||||
file_path.write_bytes(b"x")
|
||||
@@ -260,9 +260,13 @@ elif args[:3] == ['aitable', 'field', 'get']:
|
||||
"http://example.com/upload", file_path, "application/octet-stream"
|
||||
)
|
||||
)
|
||||
ok, error = IMPORT_TASK.put_file("http://example.com/upload", file_path)
|
||||
self.assertEqual(
|
||||
IMPORT_TASK.normalize_upload_url("http://example.com/upload?signature=x"),
|
||||
"https://example.com/upload?signature=x",
|
||||
)
|
||||
ok, error = IMPORT_TASK.put_file("ftp://example.com/upload", file_path)
|
||||
self.assertFalse(ok)
|
||||
self.assertIn("HTTPS", error)
|
||||
self.assertIn("HTTP(S)", error)
|
||||
|
||||
def test_export_helpers_reject_http_and_existing_output(self):
|
||||
with self.assertRaises(ValueError):
|
||||
|
||||
Reference in New Issue
Block a user