feat(cli): 错误提示行动 hint + 发布门禁 + 分发收尾 (0.1.7)

- 错误输出附 hint: 404 Invalid tableId / Workflow not found / 路由不存在 /
  401 / 403 / 非法选项值 / visibilityFilter 等 9 类模式,JSON 模式含 hint 字段
- prepublishOnly 门禁: build + test + 全命令冒烟(81 用例)
- skills/ 移除 evals/(判分标准不随技能分发) + scripts/sync-skills.sh 一键同步
- 冒烟建库固定在 agent-playground 专用空间(生产保护红线)
This commit is contained in:
yeuimu
2026-09-30 06:19:48 +00:00
parent e584b7ebbe
commit d6a8bc4063
5 changed files with 59 additions and 57 deletions
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@inkreach/inkreach-teable", "name": "@inkreach/inkreach-teable",
"version": "0.1.6", "version": "0.1.7",
"description": "CLI (command: inkreach-teable) for operating inkreach-teable: tables, fields, records, views, and the self-built automation engine (8 triggers + 7 actions).", "description": "CLI (command: inkreach-teable) for operating inkreach-teable: tables, fields, records, views, and the self-built automation engine (8 triggers + 7 actions).",
"license": "MIT", "license": "MIT",
"type": "commonjs", "type": "commonjs",
@@ -26,7 +26,8 @@
"typecheck": "tsc -p tsconfig.json --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run", "test": "vitest run",
"clean": "rimraf dist tsconfig.build.tsbuildinfo", "clean": "rimraf dist tsconfig.build.tsbuildinfo",
"prepare": "npm run build" "prepare": "npm run build",
"prepublishOnly": "npm run build && npm test && bash scripts/smoke-commands.sh"
}, },
"dependencies": { "dependencies": {
"commander": "^12.1.0" "commander": "^12.1.0"
+14 -1
View File
@@ -40,7 +40,10 @@ smoke() { # smoke <label> <cmd...>
j() { python3 -c "import sys,json;d=json.load(sys.stdin);print(eval(sys.argv[1]))" "$1"; } j() { python3 -c "import sys,json;d=json.load(sys.stdin);print(eval(sys.argv[1]))" "$1"; }
# ---------- sandbox ---------- # ---------- sandbox ----------
SPACE=$(curl -s -H "Authorization: Bearer $TOK" "$EP/api/space" | j "d[0]['id']") SPACE=$(curl -s -H "Authorization: Bearer $TOK" "$EP/api/space" | python3 -c "
import sys,json
for sp in json.load(sys.stdin):
if 'agent-playground' in sp.get('name',''): print(sp['id']); break")
BASE=$(curl -s -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \ BASE=$(curl -s -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-X POST "$EP/api/base" -d "{\"spaceId\":\"$SPACE\",\"name\":\"cli-smoke-$(date +%s)\"}" | j "d['id']") -X POST "$EP/api/base" -d "{\"spaceId\":\"$SPACE\",\"name\":\"cli-smoke-$(date +%s)\"}" | j "d['id']")
export INKREACH_TEABLE_BASE_ID="$BASE" export INKREACH_TEABLE_BASE_ID="$BASE"
@@ -171,6 +174,16 @@ fi
# ---------- plugin (reads only; register/install need a real bundle) ---------- # ---------- plugin (reads only; register/install need a real bundle) ----------
smoke "plugin list" $CLI plugin list smoke "plugin list" $CLI plugin list
smoke "plugin installs" $CLI plugin installs smoke "plugin installs" $CLI plugin installs
smoke "plugin csp list" $CLI plugin csp list
# ---------- auth token create/delete (self-cleaning PAT) ----------
PATID=$($CLI auth token create --name cli-smoke --json 2>/dev/null | j "d['id']") || PATID=""
if [ -n "${PATID:-}" ]; then
smoke "auth token create" $CLI auth token list
smoke "auth token delete" $CLI auth token delete "$PATID"
else
echo "warn auth token create returned no id (may need scopes) — check manually"
fi
# ---------- table delete (destructive, scratch only) ---------- # ---------- table delete (destructive, scratch only) ----------
smoke "table delete" $CLI table delete "$TBL" smoke "table delete" $CLI table delete "$TBL"
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Sync the repo's skills/ to every installed agent skill dir on this host.
# Single source of truth: this repo's skills/ directory.
set -eu
REPO="$(cd "$(dirname "$0")/.." && pwd)"
for d in ~/.zcode/skills ~/.agents/skills ~/.pi/agent/skills; do
[ -d "$d" ] || continue
for s in inkreach-teable inkreach-teable-plugin; do
rm -rf "$d/$s"
cp -r "$REPO/skills/$s" "$d/"
done
echo "synced -> $d"
done
-53
View File
@@ -1,53 +0,0 @@
{
"skill_name": "inkreach-teable",
"evals": [
{
"id": 1,
"name": "button-click-automation",
"prompt": "帮我在 order_logs 表里加一个按钮字段(名字用:催单按钮{TAG}),点这个按钮的时候触发一个自动化,自动化里跑一段脚本,脚本返回当前记录的订单号。然后激活它,创建一条新记录并点一下按钮测试,确认执行结果。",
"expected_output": "创建 button 字段(options.workflow 为对象 {id,...})、workflow、buttonClick 触发器、读取 triggerInput 的 script action、activate、fire button-click 测试、runs 确认 succeeded。"
},
{
"id": 2,
"name": "scheduled-digest",
"prompt": "我想要每天定时跑一次自动化(工作流名字用:每日统计{TAG}),统计 order_logs 表里的记录数并输出。帮我配好。",
"expected_output": "scheduledTime 触发器只配 timing.type + timing.interval(后端忽略其他时间字段);script/getRecords action;activate。"
},
{
"id": 3,
"name": "record-condition-email",
"prompt": "当 order_logs 表里有记录的「结果」字段变成「失败」时,自动发邮件通知 ops@xingchen.tech(工作流名字用:失败告警{TAG})。帮我配好。",
"expected_output": "recordCondition 触发器(分类是 recordCondition,条件键名是 conditions 而非 filter);sendEmail action 收件人 ops@xingchen.tech;activate。"
},
{
"id": 4,
"name": "debug-failed-run",
"prompt": "我有个自动化 wfl5F8dQRDloo28w909……不对,ID 是 {WFID},名字叫 nightly-sync,最近好像没正常跑,帮我看看是什么情况,别急着改,先诊断清楚再说。",
"expected_output": "先 workflow get 查 isActive → runs list --status failed → runs get 看 nodeResults 错误;指出脚本抛错(boom: upstream sync timeout);未盲目重新激活。"
},
{
"id": 5,
"name": "select-constraint-write",
"prompt": "在 {TBL5} 表里,把订单号为 {ORD} 的那条记录的「结果」改成「已完成」。另外告诉我这个「结果」字段目前一共有哪些可选值。",
"expected_output": "先查约束发现「已完成」不在 choices;要么说明并停下,要么先加选项再写入;不发生静默失败。"
},
{
"id": 6,
"name": "official-cli-near-miss",
"prompt": "用 teable 的 CLI 工具帮我列出这个基地里都有哪些表。",
"expected_output": "路由到 inkreach-teable CLI(官方 teable CLI 在该实例自动化契约不兼容);列出 order_logs;未安装/调用官方 teable CLI 打实例。"
},
{
"id": 7,
"name": "webhook-receiver",
"prompt": "帮我建一个自动化(名字:回单通知{TAG}),用 webhook 触发:外部系统会 POST 调用它。建好激活后,把可以调用的完整 URL 给我,并实际测试一次确认能通。",
"expected_output": "webhook 触发器;URL 形如 {endpoint}/api/webhook/{baseId}/{workflowId};用 fire webhook 或 curl 实测;已激活。"
},
{
"id": 8,
"name": "weekday-schedule-trap",
"prompt": "我要一个自动化(名字:工作日盘点{TAG}),每个工作日下午5点跑一次,统计 order_logs 表的记录条数就行。帮我配好。",
"expected_output": "知道后端只认 timing.type+interval:要么明确告知『具体时刻/星期』不被支持并给出周期间隔替代方案;绝不悄悄配 weekdays/triggerTime 假装生效。timing 配置无被忽略字段。"
}
]
}
+29 -1
View File
@@ -7,6 +7,32 @@ import { resolveConfig } from './config';
import { Client, ApiError } from './client'; import { Client, ApiError } from './client';
import { describeError } from './request'; import { describeError } from './request';
/** Actionable hints for recurring backend error shapes — the goal is that an
* agent (or human) reading the error knows the next command to try, instead
* of spending a round-trip re-deriving the cause. */
export function buildHint(status: number, message: string): string | undefined {
const m = message || '';
if (/Invalid tableId/i.test(m))
return '表不存在或不属于当前 base —— 用 `inkreach-teable table list` 核对 ID;确认 --base-id / INKREACH_TEABLE_BASE_ID 指向正确的基地。';
if (/Workflow not found/i.test(m))
return '工作流不存在(或已被软删除)—— 用 `inkreach-teable workflow list` 核对 ID;workflow delete 是软删除,列表里可能已不可见。';
if (/Cannot (GET|POST|PUT|PATCH|DELETE)/i.test(m))
return '路由不存在 —— CLI 与后端版本可能不匹配,升级 CLI:`npm i -g @inkreach/inkreach-teable`;仍失败则用 --debug 反馈。';
if (status === 401)
return '鉴权失败 —— `inkreach-teable auth status` 检查 token;必要时到 Web UI Settings → Personal Access Tokens 重新签发并 `auth login`。';
if (status === 403 || /restricted_resource|Forbidden/i.test(m))
return '权限不足 —— 该操作需要更高角色(base owner/creator)或 PAT 授权范围不含此 base;插件上传还需 plugin_developer 白名单。';
if (/Invalid value for field|Invalid option/i.test(m))
return '值不在字段合法选项内 —— `inkreach-teable field list <tableId> --with-options` 查看可选值;新增选项用 `field update <tableId> <field> --options`。';
if (/does not meet the current row conditions/i.test(m))
return '违反了字段的 visibilityFilter(行级条件)—— `inkreach-teable field visible-choices <tableId> <recordId> <field>` 查看该行可见选项。';
if (/expected array, received/i.test(m))
return '参数形状不符 —— 批量接口要数组;单条可改用 --fields,批量用 --records。';
if (/VALIDATION|Validation error/i.test(m) && status === 400)
return '请求校验失败 —— 用 --json 查看完整错误;对照 `<cmd> --help` 的参数示例。';
return undefined;
}
export interface Ctx { export interface Ctx {
client: Client; client: Client;
json: boolean; json: boolean;
@@ -62,10 +88,12 @@ export function run<TArgs extends unknown[] = unknown[]>(
await fn(ctx, positional, options); await fn(ctx, positional, options);
} catch (err) { } catch (err) {
const { status, code, message } = describeError(err); const { status, code, message } = describeError(err);
const hint = status ? buildHint(status, message) : undefined;
if (ctx.json) { if (ctx.json) {
process.stdout.write(JSON.stringify({ error: { status, code, message } }) + '\n'); process.stdout.write(JSON.stringify({ error: { status, code, message, ...(hint ? { hint } : {}) } }) + '\n');
} else { } else {
process.stderr.write(`✗ ${code}${status ? ` (${status})` : ''}: ${message}\n`); process.stderr.write(`✗ ${code}${status ? ` (${status})` : ''}: ${message}\n`);
if (hint) process.stderr.write(` hint: ${hint}\n`);
} }
process.exitCode = 1; process.exitCode = 1;
} }