Compare commits

...
9 changed files with 133 additions and 38 deletions
+26 -7
View File
@@ -17,13 +17,17 @@ metadata:
## 加载与路由顺序
1. 命中下方高频意图时直接使用精确骨架,不先查 Help 或产品级 Schema。
2. 路由优先级固定为:精确骨架 / recipe > 匹配的公开 Shortcut > 原子命令。脚本只用于 Runtime 尚未覆盖的批量、文件传输或异步编排,不与普通原子命令竞争默认入口。
1. 先定义目标对象、父容器、用户要求的阶段和完成证据;命中下方高频意图时,把精确
骨架作为首选起点,不先查 Help 或产品级 Schema。
2. 首选顺序为:精确骨架 / recipe > 匹配的公开 Shortcut > 原子命令。它用于降低
首调用成本,不是禁止依据 Runtime 新证据调整路径。脚本只用于 Runtime 尚未覆盖的
批量、文件传输或异步编排,不与普通原子命令竞争默认入口。
3. 参数、约束或安全语义不确定时只读 leaf Schema:`dws schema --cli-path "aitable <leaf>" --format json`;只有当前 Cobra flag 不确定时才读对应 `--help`。
4. 复杂字段、筛选、导入导出、视图、权限或工作流任务,按“低频能力与 Reference”只加载相关文件,不预读整个 `references/aitable/`。
5. 现有骨架和 reference 都无法定位能力时,才用 Runtime Shortcut Catalog 做最后发现;不得猜 `cli_path` 或 flag。
6. Schema、Help、reference 与实际返回冲突时采用更安全的解释并报告契约漂移;`confirmation=user_required` 时先确认,再添加 `--yes`。
7. 用户已给足目标、字段和数据时,按依赖链连续执行;中间结果只用于提取真实 ID 和判断停止条件,全部完成后统一回读并答复。
7. 用户已给足目标、字段和数据时,按依赖链连续执行;保留已成功步骤及真实 ID,只从
未完成缺口继续。每个关键结论用语义等价证据关闭后再统一答复。
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcut 发现(按需)
@@ -58,12 +62,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 widgets-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 +79,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` 等值。
## 写入计划与验证
@@ -94,6 +100,11 @@ metadata:
退出码 0、`status=success`、空对象或仅有 taskId 都不能单独证明业务完成。字段未生效、回读不一致、分页不完整或异步任务未完成时,报告失败、部分完成或进行中,不得声称“全部完成”。
验证必须与用户点名的操作语义一致:Base 搜索只能由搜索结果中的目标命中关闭,最近
访问列表或创建回读只能证明对象存在;复制必须有复制操作产生的新对象证据;移动必须
回读新的父级关系;删除必须核对精确目标并确认其已不存在。邻近证据可用于诊断,不能
替代目标步骤的完成证据。
## 低频能力与 Reference
| 场景 | 按需读取 |
@@ -108,10 +119,18 @@ metadata:
## 错误恢复
- 路径或 flag 错误:按既定的 leaf Schema → leaf Help 顺序校正一次;仍失败则停止,不连续尝试猜测别名。
- 不使用全局“只准纠正一次”的硬限制。恢复预算由已观察状态和副作用风险决定,只在
新错误信息、leaf Schema/Help 或回读产生新证据时继续;重复同一假设或不再增加证据
时停止。
- 路径、flag 或 payload 在写入前被校验拒绝:吸收精确 hint,保持目标和容器不变后
修正。只读调用可做有界替代探测,但不得用相邻能力冒充目标能力。
- 命令非零、输出非 JSON、业务 `status != success`、必需 ID 缺失、批处理部分失败均视为失败;保留成功项与 ledger,禁止吞错。
- 同名歧义、权限不足、资源不存在、字段类型漂移、分页无法推进或 Schema/Help 冲突时停止并报告。具体恢复动作按需读 [aitable-error-recovery.md](references/aitable/aitable-error-recovery.md)。
- 已确认远端未写入且契约声明可重试时,才从最新实际输出重新提取 ID 后重试;写入状态未知、删除和其他 `confirmation=user_required` 操作不得自动重试或静默确认。
- 写入已返回 ID 时保留成功状态,只补字段、记录、视图、图表或验证缺口。写入超时或
状态未知时先回读当前 Base/Table;只有明确未写入且契约允许时才重试。删除和其他
`confirmation=user_required` 操作不得自动重试或静默确认。
- 不要重复猜同一 flag、formatter、folderId 或图表配置。目标能力已有原生命令时,
不要以手工重建相似对象掩盖原命令失败。
## 跨产品协作
@@ -5,9 +5,10 @@
## 建议操作顺序
```bash
# 1) 先看配置模板(JSONC)
# 1) 先看图表配置模板和当前 leaf 契约
dws aitable dashboard config-example --format json
dws aitable chart widgets-example --format json
dws aitable chart create --help
# 2) 先拿 dashboard,再拿 chart 详情
dws aitable dashboard get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --format json
@@ -36,11 +37,15 @@ dws aitable chart get --base-id <BASE_ID> --dashboard-id <DASHBOARD_ID> --chart-
| 命令 | 用途 | 必填参数 |
|------|------|----------|
| `chart get` | 获取图表详情 | `--base-id` `--dashboard-id` `--chart-id` |
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` |
| `chart create` | 创建图表 | `--base-id` `--dashboard-id` `--config` `--layout` |
| `chart update` | 更新图表配置 | `--base-id` `--dashboard-id` `--chart-id` `--config` |
| `chart delete` | 删除图表 | `--base-id` `--dashboard-id` `--chart-id` `--yes` |
| `chart widgets-example` | 查看图表 widgets 配置模板 | 无 |
## 配置获取流程
创建图表前,必须先调用 `chart widgets-example` 查看配置模板,了解每种图表类型需要的字段结构,然后根据实际 tableId 和 fieldId 填充配置。
创建图表前,先调用当前 Runtime 的 `chart widgets-example`(如未来命令发生变化,
以 leaf Schema/Help 的真实命令为准),再根据实际 tableId 和 fieldId 填充配置,并
在第一次创建时同时传入 `--config` 与 `--layout`。创建返回
`chartId` 后用 `dashboard get`/`chart get` 回读;只有 Dashboard 存在但图表缺失时,
图表阶段仍未完成。
@@ -70,22 +70,27 @@
| 导入文件格式错误 | 不支持的文件格式或文件损坏 | 确认文件为 .xlsx 格式且未加密 |
| 批处理只成功一部分 | 某批或某个字段返回失败 | 保留成功 ID,输出失败 batch/item ledger,以非零状态结束;不要整批重放 |
## 3. 重试策略
## 3. 状态感知恢复策略
### 3.1 可重试的错误
不要用一个全局次数限制处理所有错误。先判断上一次调用是否可能产生副作用,再决定
探测、继续或停止。
| 错误类型 | 重试方式 | 最大重试次数 |
|---------|---------|------------|
| 网络超时 / 5xx | 等待 2s 后原样重试 | 2 |
| 导出任务未完成 | 轮询 task-id | 5(间隔 3s) |
| 并发写入冲突 | 串行重试 | 1 |
| 已观察状态 | 恢复动作 |
|---|---|
| 只读调用失败 | 根据精确错误或 leaf 契约做有界探测;目标和父容器保持不变 |
| 写入前参数校验失败 | 确认无副作用后修正 payload/flag;不原样重试 |
| 网络超时 / 5xx,写入状态未知 | 先回读目标 Base/Table,确定未生效后才考虑重试 |
| 已返回新对象或记录 ID | 绑定该 ID,只继续尚未完成的步骤 |
| 异步任务未完成 | 继续查询同一个 taskId/importId,直到契约规定的终态或总超时 |
| 并发冲突且明确未写入 | 降低并发或串行重试;再次冲突时停止并报告 |
| 删除或高影响操作状态未知 | 停止并回读,不自动重放 |
### 3.2 不可重试的错误(立即停止)
| 错误类型 | 原因 | 处理方式 |
|---------|------|---------|
| 权限不足 / 403 | 用户对该 Base 无权限 | 停止操作,提示用户确认权限 |
| 参数格式错误 | 请求结构不合法 | 修正参数后重试,不要原样重试 |
| 参数格式错误 | 请求结构不合法 | 只在校验明确未写入且有新契约证据时修正,不要原样重试 |
| 资源不存在 / 404 | ID 错误或资源已删除 | 重新查询定位资源 |
| 配额超限 / 429 | API 调用频率过高 | 等待后重试,并降低并发 |
@@ -94,7 +99,12 @@
在重试前,先确认:
1. ❓ 错误是暂时性的还是永久性的?
2. ❓ 参数有没有明显错误需要修正?
3. ❓ 是否需要先查询最新状态再重试?
3. ❓ 上次调用是否可能已经写入?
4. ❓ 是否已经获得对象 ID,应该从后续缺口继续?
5. ❓ 新尝试是否基于新证据,而不是重复同一猜测?
当没有新证据、开始重复同一错误、目标发生变化或副作用无法判断时停止。Base 搜索
失败时,`base list` 只能帮助确认对象存在,不能作为搜索完成证据。
## 4. 调试技巧
@@ -40,8 +40,8 @@ dws aitable import upload --base-id <BASE_ID> \
--file-name data.xlsx --file-size <字节数> --format json
# → 返回 uploadUrl 和 importId
# 第 2 步:上传文件到 OSS(注意:Content-Type 必须设为空)
curl -X PUT "<uploadUrl>" -H "Content-Type:" --data-binary @data.xlsx
# 第 2 步:由受测 helper 上传到 OSS(Content-Type 为空)
# 正常 Agent 执行使用 aitable_import_via_task.py,不手工拆出 curl fallback。
# 第 3 步:触发导入(新建表模式)
dws aitable import data --import-id <importId> --format json
@@ -57,7 +57,7 @@ dws aitable import data --import-id <importId> --table-id <TABLE_ID> --format js
| 步骤 | 命令 | 说明 |
|------|------|------|
| 申请上传凭证 | `import upload --base-id <ID> --file-name <名称> --file-size <字节>` | `--file-size` 必须与实际文件大小一致 |
| 上传文件 | HTTP PUT(curl 等) | **必须** 带 `-H "Content-Type:"` 将 Content-Type 设为空,否则 OSS 返回 403 |
| 上传文件 | 受测 helper 的 HTTP PUT | Content-Type 必须为空,否则 OSS 返回 403;helper 需校验 OSS host,并将受信 HTTP signed URL 升级为 HTTPS |
| 触发导入 | `import data --import-id <ID> [--table-id <TABLE_ID>]` | 同步等待,大多一次调用即返回结果;超时可用相同 importId 重试 |
### import data 参数
@@ -104,7 +104,9 @@ dws aitable import data --import-id <importId> --table-id <TABLE_ID> --format js
> **解决方案**:
> 1. **首选**:创建目标表时,字段名与 Excel 表头列名**保持完全一致**
> 2. **备选**:传 `--field-mapping '{"目标字段名":"Excel列名"}'` 手动指定映射
> 3. **兜底**:如果 import data 多次失败,改用 `record create` 逐条写入
> 3. 如果 import data 失败,先按错误和任务状态判断是否已写入;只有用户目标允许
> “追加已有表”、文件内容可安全解析且新建表导入明确未生效时,才提出改用
> `record create`。逐条写入不等价于文件级导入,不能静默替代并宣称导入成功。
### 适用场景
@@ -38,6 +38,15 @@
## 字段创建时设置 config(重要)
字段 JSON 的命名和枚举是严格契约:
- 批量 `--fields` 每项使用 `fieldName`,不是 `name`。
- 单选是 `singleSelect`,多选是 `multipleSelect`;不存在通用的 `select` 类型。
- number/date/currency formatter 只使用当前 `table create --help` 或 leaf Schema 列出的枚举;不要从别的产品或旧样例猜 `INTEGER`。
- 返回明确 hint 时,只修改 hint 指向的字段契约并保持目标不变。只要写入前校验明确
未产生副作用且新的 Schema/Help 证据仍在收敛,就可继续修正;重复同一假设或不再
获得新证据时停止并保留部分成功 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)
+18 -4
View File
@@ -39,6 +39,16 @@ skill 作为入口进行二级路由。
- 创建、更新、发送等写操作后,按产品 skill 的要求回读或查询状态,不得仅凭退出码
宣称成功。
## 先定义结果,再选择命令
执行前把请求压缩成一个结果契约:目标状态、资源类型与父容器、用户明确要求的阶段、
副作用边界,以及每个关键结论需要的验证证据。命令只是达到结果的候选路径,不得用
相似终态替代用户点名的操作:新建相似对象不等于复制成功,最近访问列表不等于搜索
命中,发送请求返回成功也不等于对方已收到。
执行过程中维护轻量状态账本:已确认成功的步骤和 ID、未完成缺口、写入状态是否确定、
下一步安全探测及最终验证。恢复时从缺口继续,不重放已经成功的写操作。
## 渐进加载
只读取当前任务需要的文件,不要一次性加载全部 shared references:
@@ -82,8 +92,12 @@ skill 作为入口进行二级路由。
## 错误最短路径
1. `unknown command` / `unknown flag`:运行对应层级 `--help`,修正后最多重试一次。
1. `unknown command` / `unknown flag`:读取错误中的 hint、leaf Schema 或对应层级
`--help`,只在获得新证据时修正;不要重复尝试同一假设。
2. 认证或权限错误:读取 `global-reference.md` 与 `error-codes.md` 对应章节。
3. 其他错误:加 `--verbose` 重试一次;仍失败则停止并报告真实错误,不连续尝试替代
命令。
4. 明确不支持的能力:说明边界,不通过其他接口绕过。
3. 只读调用或写入前参数校验失败:保持目标不变,依据新证据做有界恢复。
4. 写入已返回 ID:保留成功状态,只补后续缺口;写入超时、返回不明或可能已生效时,
先回读目标容器,禁止盲目重放。删除及高影响操作状态不明时停止并报告。
5. 当后续尝试不再产生新证据、开始重复同一错误、改变目标或副作用变得不确定时停止;
不用不同产品或相似对象掩盖原操作失败。
6. 明确不支持的能力:说明边界,不通过其他接口绕过。