Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71b45b1eab |
@@ -58,12 +58,13 @@ metadata:
|
||||
| 按名称找 Table | `dws aitable +resolve-table --base <baseId> --name "<表名>" --format json` | `baseId` 必须来自上一步真实返回 |
|
||||
| 取表、字段与视图目录 | `dws aitable +table-get --base-id <baseId> [--table-ids <tableId>] --format json` | `tables[].fields[]` 是字段目录;完整类型/config 再用 `+field-get` |
|
||||
| 取字段完整配置 | `dws aitable +field-get --base-id <baseId> --table-id <tableId> [--field-ids <ids>] --format json` | 写入前核对类型、只读性和 select options;按需展开以控制返回体 |
|
||||
| 查/搜/筛记录 | `dws aitable +record-query --base-id <baseId> --table-id <tableId> [--query <词>\|--filters '<JSON>'\|--record-ids <ids>] --format json` | ID 模式忽略 filter/sort;全量结论必须完整分页 |
|
||||
| 查/搜/筛记录 | `dws aitable +record-query --base-id <baseId> --table-id <tableId> [--query <词>\|--filters '<JSON>'\|--record-ids <ids>] --format json` | ID 模式忽略 filter/sort;不要猜 `--page-limit`,分页使用返回的 cursor 与 leaf Help 中的真实分页 flag |
|
||||
| 新增记录 | `dws aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":<值>}}]' --format json` | 单次最多 100;取 `data.newRecordIds[]` 后立即按 ID 回读 |
|
||||
| 更新记录 | `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 "<名>"` / `dws aitable table create --base-id <id> --name "<名>" --fields '[...]'` | 使用创建返回的真实 ID;系统改名/加后缀时不得继续猜原名 |
|
||||
| 复制视图 | `dws aitable view duplicate --base-id <baseId> --table-id <tableId> --view-id <viewId> --new-name "<名>"` | `viewId` 必须属于当前表;不能用复制 Table 或新建 Dashboard 替代 |
|
||||
| 创建图表 | 先读 `dws aitable chart config-example --format json`,再执行 `chart create ... --config '<JSON>' --layout '<JSON>'` | `--layout` 是必填项;不能只传 config 后依据退出码声称图表已创建 |
|
||||
| 批量追加 CSV / JSON 到已有表 | `python3 scripts/import_records.py <baseId> <tableId> <file> [batch_size]` | CSV 表头必须是 fieldId;脚本返回不完整 ledger 时不得宣称全成功 |
|
||||
| 文件导入为新数据表 | `python3 scripts/aitable_import_via_task.py <baseId> <file>` | 与“追加已有 table”不同;走 prepare → PUT → import task |
|
||||
| 批量创建字段 | `python3 scripts/bulk_add_fields.py <baseId> <tableId> fields.json` | 单次最多 15;逐项检查成功/失败结果 |
|
||||
@@ -74,9 +75,10 @@ metadata:
|
||||
|
||||
- `record create/update` 前必须获取目标字段的 `fieldId`、`type` 与 `config`;`filterUp`、`lookup` 等只读字段不可写。完整格式只在需要时读 [aitable-cell-value.md](references/aitable/aitable-cell-value.md)。
|
||||
- 筛选和排序字段使用 `fieldId`;`--filters` 最外层是 `and|or + operands`,`--sort` 使用 `direction: asc|desc`。日期和跨表字段规则按需读 [aitable-filter-sort.md](references/aitable/aitable-filter-sort.md)。
|
||||
- `record query --all` 仍受 `--page-limit` 约束;分页中断或局部富化失败时保留已有结果,输出 completeness 与逐项失败 ledger,不把部分结果描述为全量。
|
||||
- 记录分页不能凭经验拼 `--page-limit`。先用当前 leaf Help 确认 page-size/cursor 的真实名称;每页读取返回 cursor,直到明确终止。分页中断或局部富化失败时保留已有结果,输出 completeness 与逐项失败 ledger,不把部分结果描述为全量。
|
||||
- 创建、更新、导入、批量建字段等写操作必须检查业务 `status`、逐项结果与返回 ID;普通写入按用户明确要求执行后回读,不能只凭退出码宣称成功。
|
||||
- 长 JSON 使用 `--records-file` / 任务文件;不得为绕过字段错误而静默丢列、改类型或删除失败项。
|
||||
- `table create --fields` 的键固定为 `fieldName` / `type` / `config`,不能写成 `name`;单选/多选类型固定为 `singleSelect` / `multipleSelect`,不能写 `select`。number formatter 不确定时先读 leaf Help,禁止猜 `INTEGER` 等值。
|
||||
|
||||
## 写入计划与验证
|
||||
|
||||
@@ -112,6 +114,7 @@ metadata:
|
||||
- 命令非零、输出非 JSON、业务 `status != success`、必需 ID 缺失、批处理部分失败均视为失败;保留成功项与 ledger,禁止吞错。
|
||||
- 同名歧义、权限不足、资源不存在、字段类型漂移、分页无法推进或 Schema/Help 冲突时停止并报告。具体恢复动作按需读 [aitable-error-recovery.md](references/aitable/aitable-error-recovery.md)。
|
||||
- 已确认远端未写入且契约声明可重试时,才从最新实际输出重新提取 ID 后重试;写入状态未知、删除和其他 `confirmation=user_required` 操作不得自动重试或静默确认。
|
||||
- 参数校验错误必须先吸收错误中的精确 hint,再最多纠正一次;不要重复猜同一 flag、formatter、folderId 或图表配置。目标能力已有原生命令时,不要以手工重建相似对象掩盖原命令失败。
|
||||
|
||||
## 跨产品协作
|
||||
|
||||
|
||||
@@ -38,6 +38,13 @@
|
||||
|
||||
## 字段创建时设置 config(重要)
|
||||
|
||||
字段 JSON 的命名和枚举是严格契约:
|
||||
|
||||
- 批量 `--fields` 每项使用 `fieldName`,不是 `name`。
|
||||
- 单选是 `singleSelect`,多选是 `multipleSelect`;不存在通用的 `select` 类型。
|
||||
- number/date/currency formatter 只使用当前 `table create --help` 或 leaf Schema 列出的枚举;不要从别的产品或旧样例猜 `INTEGER`。
|
||||
- 第一次返回明确 hint 后只按 hint 修正一次;第二次仍失败就停止并保留部分成功 ledger。
|
||||
|
||||
创建 singleSelect/multipleSelect 字段时,**必须设置选项 (options)**:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -26,7 +27,7 @@ from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{6,128}$")
|
||||
ALLOWED_FORMATS = {"excel", "attachment", "excel_and_attachment", "excel_with_inline_images"}
|
||||
|
||||
|
||||
@@ -64,7 +65,12 @@ def normalize_download_url(url: str) -> str:
|
||||
def download_file(url: str, output_path: Path) -> Tuple[bool, str]:
|
||||
req = Request(url, method="GET")
|
||||
try:
|
||||
with urlopen(req, timeout=180) as resp:
|
||||
try:
|
||||
import certifi
|
||||
context = ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
context = ssl.create_default_context()
|
||||
with urlopen(req, timeout=180, context=context) as resp:
|
||||
redirected = urlparse(resp.geturl())
|
||||
if redirected.scheme != "https" or not redirected.hostname:
|
||||
return False, "download redirect is not HTTPS"
|
||||
|
||||
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -25,7 +26,7 @@ from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
|
||||
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{6,128}$")
|
||||
ALLOWED_EXTENSIONS = {".csv", ".xlsx", ".xls"}
|
||||
|
||||
|
||||
@@ -33,6 +34,24 @@ def validate_resource_id(resource_id: str) -> bool:
|
||||
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
|
||||
|
||||
|
||||
def normalize_oss_upload_url(upload_url: str) -> str:
|
||||
"""Accept the signed OSS URL returned by Runtime and upgrade it to HTTPS."""
|
||||
parsed = urlparse(upload_url)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
||||
raise ValueError("uploadUrl must be a valid HTTP(S) URL")
|
||||
if not parsed.hostname.endswith(".oss.aliyuncs.com"):
|
||||
raise ValueError("uploadUrl host is not an Aliyun OSS endpoint")
|
||||
return parsed._replace(scheme="https").geturl()
|
||||
|
||||
|
||||
def tls_context() -> ssl.SSLContext:
|
||||
try:
|
||||
import certifi
|
||||
return ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
return ssl.create_default_context()
|
||||
|
||||
|
||||
def run_dws(dws_bin: str, args: list[str], timeout_sec: int = 120) -> Tuple[int, str, str]:
|
||||
cmd = [dws_bin] + args
|
||||
try:
|
||||
@@ -53,15 +72,16 @@ def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
|
||||
|
||||
|
||||
def put_file(upload_url: str, file_path: Path) -> Tuple[bool, str]:
|
||||
parsed = urlparse(upload_url)
|
||||
if parsed.scheme != "https" or not parsed.hostname:
|
||||
return False, "uploadUrl must be a valid HTTPS URL"
|
||||
try:
|
||||
upload_url = normalize_oss_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")
|
||||
# 关键:清空 Content-Type,避免 SignatureDoesNotMatch。
|
||||
req.add_header("Content-Type", "")
|
||||
try:
|
||||
with urlopen(req, timeout=180) as resp:
|
||||
with urlopen(req, timeout=180, context=tls_context()) as resp:
|
||||
if resp.status == 200:
|
||||
return True, ""
|
||||
return False, f"unexpected HTTP status: {resp.status}"
|
||||
|
||||
@@ -25,13 +25,14 @@ import subprocess
|
||||
import os
|
||||
import mimetypes
|
||||
import re
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
|
||||
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
|
||||
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{6,128}$')
|
||||
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
|
||||
|
||||
|
||||
@@ -77,15 +78,24 @@ def run_dws(args: list, dws_bin: str = 'dws') -> Optional[Dict[str, Any]]:
|
||||
def upload_to_oss(upload_url: str, file_path: Path, mime_type: str) -> bool:
|
||||
"""通过 HTTP PUT 上传文件到 OSS。"""
|
||||
parsed = urlparse(upload_url)
|
||||
if parsed.scheme != 'https' or not parsed.hostname:
|
||||
print('错误:uploadUrl 必须是有效的 HTTPS URL', file=sys.stderr)
|
||||
if parsed.scheme not in {'http', 'https'} or not parsed.hostname:
|
||||
print('错误:uploadUrl 必须是有效的 HTTP(S) URL', file=sys.stderr)
|
||||
return False
|
||||
if not parsed.hostname.endswith('.oss.aliyuncs.com'):
|
||||
print('错误:uploadUrl 不是阿里云 OSS 地址', file=sys.stderr)
|
||||
return False
|
||||
upload_url = parsed._replace(scheme='https').geturl()
|
||||
file_data = file_path.read_bytes()
|
||||
req = Request(upload_url, data=file_data, method='PUT')
|
||||
req.add_header('Content-Type', mime_type)
|
||||
|
||||
try:
|
||||
with urlopen(req, timeout=120) as resp:
|
||||
try:
|
||||
import certifi
|
||||
context = ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
context = ssl.create_default_context()
|
||||
with urlopen(req, timeout=120, context=context) as resp:
|
||||
if resp.status == 200:
|
||||
return True
|
||||
print(f"错误:OSS 上传失败,HTTP {resp.status}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user