fix(cli): 轨迹分析驱动的 8 处命令修复 + 技能文档同步 (0.1.5)

- table get/update: 改用真实路由 /api/base/:baseId/table/:tableId(/name)
- view get/update/delete: table 域路由;view update 实现文档承诺的
  --filter/--sort/--group/--column-meta(sort 自动包装 sortObjs)
- record create --fields 自动包装 {records:[{fields}]}
- record delete 单条走单删端点(修复单值 query 解析成 string 的 400),
  支持位置参数;record list 加为 record get 别名
- record get-one / record get --record-id 走 table 域路由
- 移除 sql 命令:后端无 SQL 查询 API(404),技能文档 5 处同步改写
- plugin 技能 dev-workflow 内联 known-good webpack.config
  (禁止 output.library.type=iife,评估轨迹中 with_skill 组也反复踩坑)
This commit is contained in:
yeuimu
2026-09-29 06:45:37 +00:00
parent fbff682d2c
commit 8dacbf7f2d
11 changed files with 198 additions and 124 deletions
+1 -2
View File
@@ -10,7 +10,7 @@ import { pluginCommand } from './commands/plugin';
import { authCommand } from './commands/auth';
import { workflowCommand, triggerCommand, actionCommand } from './commands/workflow';
import { baseResourceCommands } from './commands/resources';
import { secretCommand, runsCommand, fireCommand, sqlCommand } from './commands/extras';
import { secretCommand, runsCommand, fireCommand } from './commands/extras';
import { subscriptionCommand } from './commands/subscription';
const program = new Command();
@@ -42,7 +42,6 @@ program.addCommand(subscriptionCommand());
program.addCommand(secretCommand());
program.addCommand(runsCommand());
program.addCommand(fireCommand());
program.addCommand(sqlCommand());
program.parseAsync(process.argv).catch((err) => {
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
-47
View File
@@ -2,7 +2,6 @@
* `inkreach-teable secret` — manage workflow automation secrets.
* `inkreach-teable runs` — execution history.
* `inkreach-teable fire` — manual trigger testing (button-click / webhook).
* `inkreach-teable sql` — read-only SQL queries.
*/
import { Command } from 'commander';
import { run, requireBaseId } from '../lib/ctx';
@@ -282,49 +281,3 @@ export function fireCommand(): Command {
return cmd;
}
// ===================== sql =====================
export function sqlCommand(): Command {
const cmd = new Command('sql');
cmd.description('Execute a read-only SQL query (SELECT only)');
cmd.option('--base-id <id>', 'Base id (overrides default)');
cmd.argument('<sql>', 'SQL SELECT statement');
cmd.action(
run(async (ctx, args, o) => {
const sql = args[0] as string;
const opts = o as { baseId?: string };
const baseId = requireBaseId(ctx, opts.baseId);
const rows = (await ctx.client.sql(baseId, sql)) as unknown[];
if (ctx.json) {
printJson(rows);
} else {
if (!rows.length) {
process.stdout.write('No rows returned.\n');
return;
}
const first = rows[0];
if (typeof first === 'object' && first !== null && !Array.isArray(first)) {
const keys = Object.keys(first as Record<string, unknown>);
printTable(
keys.map((k) => ({ key: k, header: k })),
rows.map((r) => {
const obj = r as Record<string, unknown>;
const row: Record<string, unknown> = {};
for (const k of keys) {
row[k] = truncate(obj[k], 40);
}
return row;
})
);
process.stdout.write(`\n${rows.length} row(s).\n`);
} else {
rows.forEach((r) => process.stdout.write(JSON.stringify(r) + '\n'));
}
}
})
);
return cmd;
}
+79 -22
View File
@@ -140,10 +140,12 @@ function tableCmd(): Command {
cmd
.command('get <tableId>')
.description('Get table details')
.option('--base-id <id>', 'Base id (overrides default)')
.action(
run(async (ctx, args) => {
run(async (ctx, args, o) => {
const tableId = args[0] as string;
const table = await ctx.client.getTable(tableId);
const opts = o as { baseId?: string };
const table = await ctx.client.getTable(tableId, requireBaseId(ctx, opts.baseId));
if (ctx.json) printJson(table);
else {
const t = table as Record<string, unknown>;
@@ -469,8 +471,10 @@ function recordCmd(): Command {
cmd
.command('get')
.description('Query records with filters, pagination, and search')
.description("Query records with filters, pagination, and search (alias: 'list')")
.alias('list')
.requiredOption('--table-id <id>', 'Table id')
.option('--record-id <id>', 'Get a single record by id (alternative to get-one)')
.option('--filter <json>', 'Filter conditions JSON')
.option('--projection <json>', 'Field projection JSON array')
.option('--page-size <n>', 'Records per page')
@@ -487,7 +491,17 @@ function recordCmd(): Command {
page?: string;
search?: string;
baseId?: string;
recordId?: string;
};
if (opts.recordId) {
const rec = await ctx.client.getRecord(opts.tableId, opts.recordId);
if (ctx.json) printJson(rec);
else {
const r = rec as { id?: string; fields?: Record<string, unknown> };
printKv([['id', r.id], ...Object.entries(r.fields || {})]);
}
return;
}
// validate baseId is present (even though getRecords only needs tableId,
// we follow the convention that base-scoped commands require baseId config)
requireBaseId(ctx, opts.baseId);
@@ -543,10 +557,12 @@ function recordCmd(): Command {
cmd
.command('get-one <recordId>')
.description('Get a single record by id')
.requiredOption('--table-id <id>', 'Table id (record routes are table-scoped)')
.action(
run(async (ctx, args) => {
run(async (ctx, args, o) => {
const recordId = args[0] as string;
const record = await ctx.client.getRecord(recordId);
const opts = o as { tableId: string };
const record = await ctx.client.getRecord(opts.tableId, recordId);
if (ctx.json) printJson(record);
else {
const rec = record as { id?: string; fields?: Record<string, unknown> };
@@ -572,7 +588,9 @@ function recordCmd(): Command {
if (opts.records) {
body = { records: JSON.parse(opts.records) };
} else if (opts.fields) {
body = { fields: JSON.parse(opts.fields) };
// The create API only accepts {records:[...]}; wrap the single-record
// convenience form so both spellings work.
body = { records: [{ fields: JSON.parse(opts.fields) }] };
} else {
throw Object.assign(new Error('Provide --fields or --records'), {
code: 'MISSING_FIELDS',
@@ -603,17 +621,33 @@ function recordCmd(): Command {
);
cmd
.command('delete')
.command('delete [recordIds...]')
.description('Batch delete records by ids')
.requiredOption('--table-id <id>', 'Table id')
.requiredOption('--record-ids <csv>', 'Record ids (comma-separated)')
.option('--record-ids <csv>', 'Record ids (comma-separated; or pass ids as arguments)')
.action(
run(async (ctx, _args, o) => {
const opts = o as { tableId: string; recordIds: string };
const ids = opts.recordIds
.split(',')
.map((s) => s.trim())
.filter(Boolean);
run(async (ctx, args, o) => {
const opts = o as { tableId: string; recordIds?: string };
// With variadic args commander appends the options bag to positionals —
// flatten and keep only actual id strings.
const ids = [
...new Set(
[
...(opts.recordIds
? opts.recordIds
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: []),
...(args as unknown[]).flat().filter((x): x is string => typeof x === 'string'),
].flat()
),
];
if (!ids.length) {
throw Object.assign(new Error('Provide record ids (--record-ids or arguments)'), {
code: 'MISSING_RECORD_IDS',
});
}
const result = await ctx.client.deleteRecords(opts.tableId, ids);
if (ctx.json) printJson(result);
else process.stdout.write(`✓ ${ids.length} record(s) deleted\n`);
@@ -661,10 +695,12 @@ function viewCmd(): Command {
cmd
.command('get <viewId>')
.description('Get view details')
.requiredOption('--table-id <id>', 'Table id (view routes are table-scoped)')
.action(
run(async (ctx, args) => {
run(async (ctx, args, o) => {
const viewId = args[0] as string;
const view = await ctx.client.getView(viewId);
const opts = o as { tableId: string };
const view = await ctx.client.getView(opts.tableId, viewId);
if (ctx.json) printJson(view);
else {
const v = view as Record<string, unknown>;
@@ -698,15 +734,34 @@ function viewCmd(): Command {
cmd
.command('update <viewId>')
.description('Update a view (rename)')
.description('Update a view (name, description, filter, sort, group, column-meta)')
.requiredOption('--table-id <id>', 'Table id (view routes are table-scoped)')
.option('--name <name>', 'New view name')
.option('--description <d>', 'New description')
.option('--filter <json>', 'Filter JSON, e.g. \'{"conjunction":"and","filterSet":[...]}\'')
.option('--sort <json>', 'Sort JSON, e.g. \'[{"fieldId":"fldX","order":"asc"}]\'')
.option('--group <json>', 'Group JSON, e.g. \'[{"fieldId":"fldX","order":"asc"}]\'')
.option('--column-meta <json>', 'Column meta JSON array of {fieldId, columnMeta}')
.action(
run(async (ctx, args, o) => {
const viewId = args[0] as string;
const opts = o as { name?: string };
const body: Record<string, unknown> = {};
const opts = o as {
tableId: string;
name?: string;
description?: string;
filter?: string;
sort?: string;
group?: string;
columnMeta?: string;
};
const body: Parameters<typeof ctx.client.updateView>[2] = {};
if (opts.name) body.name = opts.name;
const view = await ctx.client.updateView(viewId, body);
if (opts.description) body.description = opts.description;
if (opts.filter) body.filter = JSON.parse(opts.filter);
if (opts.sort) body.sort = JSON.parse(opts.sort);
if (opts.group) body.group = JSON.parse(opts.group);
if (opts.columnMeta) body.columnMeta = JSON.parse(opts.columnMeta);
const view = await ctx.client.updateView(opts.tableId, viewId, body);
if (ctx.json) printJson(view);
else process.stdout.write(`✓ View ${viewId} updated\n`);
})
@@ -715,10 +770,12 @@ function viewCmd(): Command {
cmd
.command('delete <viewId>')
.description('Delete a view')
.requiredOption('--table-id <id>', 'Table id (view routes are table-scoped)')
.action(
run(async (ctx, args) => {
run(async (ctx, args, o) => {
const viewId = args[0] as string;
await ctx.client.deleteView(viewId);
const opts = o as { tableId: string };
await ctx.client.deleteView(opts.tableId, viewId);
if (ctx.json) printJson({ ok: true, id: viewId });
else process.stdout.write(`✓ View ${viewId} deleted\n`);
})
+55 -18
View File
@@ -59,14 +59,19 @@ export class Client {
listTables(baseId: string) {
return this.call<unknown[]>('GET', `/api/base/${baseId}/table`);
}
getTable(tableId: string) {
return this.call<unknown>('GET', `/api/table/${tableId}`);
getTable(tableId: string, baseId?: string) {
// Route is base-nested: GET /api/base/:baseId/table/:tableId (a bare
// /api/table/:tableId does not exist and 404s).
const base = baseId ?? this.cfg.baseId;
return this.call<unknown>('GET', `/api/base/${base}/table/${tableId}`);
}
createTable(baseId: string, body: unknown) {
return this.call<unknown>('POST', `/api/base/${baseId}/table`, body);
}
updateTable(tableId: string, body: unknown) {
return this.call<unknown>('PUT', `/api/table/${tableId}`, body);
updateTable(tableId: string, body: { name: string }, baseId?: string) {
// Rename goes through the /name sub-resource (a bare PUT .../table/:id 404s).
const base = baseId ?? this.cfg.baseId;
return this.call<unknown>('PUT', `/api/base/${base}/table/${tableId}/name`, body);
}
deleteTable(tableId: string, baseId?: string) {
const base = baseId ?? this.cfg.baseId;
@@ -131,9 +136,10 @@ export class Client {
}
return out;
}
getRecord(recordId: string, query?: Record<string, unknown>) {
getRecord(tableId: string, recordId: string, query?: Record<string, unknown>) {
// Route is table-scoped: GET /api/table/:tableId/record/:recordId
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
return this.call<unknown>('GET', `/api/record/${recordId}${qs}`);
return this.call<unknown>('GET', `/api/table/${tableId}/record/${recordId}${qs}`);
}
createRecord(tableId: string, body: unknown) {
return this.call<unknown>('POST', `/api/table/${tableId}/record`, body);
@@ -141,10 +147,15 @@ export class Client {
updateRecord(tableId: string, body: unknown) {
return this.call<unknown>('PATCH', `/api/table/${tableId}/record`, body);
}
updateRecordOne(recordId: string, body: unknown) {
return this.call<unknown>('PATCH', `/api/record/${recordId}`, body);
updateRecordOne(tableId: string, recordId: string, body: unknown) {
return this.call<unknown>('PATCH', `/api/table/${tableId}/record/${recordId}`, body);
}
deleteRecords(tableId: string, recordIds: string[]) {
if (recordIds.length === 1) {
// Bulk query ?recordIds=x parses as a string and 400s — single deletes
// use the per-record route.
return this.call<unknown>('DELETE', `/api/table/${tableId}/record/${recordIds[0]}`);
}
const qs = recordIds.map((id) => `recordIds=${encodeURIComponent(id)}`).join('&');
return this.call<unknown>('DELETE', `/api/table/${tableId}/record?${qs}`);
}
@@ -153,17 +164,47 @@ export class Client {
listViews(tableId: string) {
return this.call<unknown[]>('GET', `/api/table/${tableId}/view`);
}
getView(viewId: string) {
return this.call<unknown>('GET', `/api/view/${viewId}`);
getView(tableId: string, viewId: string) {
// View routes are table-scoped: /api/view/:viewId alone does not exist.
return this.call<unknown>('GET', `/api/table/${tableId}/view/${viewId}`);
}
createView(tableId: string, body: unknown) {
return this.call<unknown>('POST', `/api/table/${tableId}/view`, body);
}
updateView(viewId: string, body: unknown) {
return this.call<unknown>('PATCH', `/api/view/${viewId}`, body);
/** Update view parts — each key goes to its own sub-resource route. */
async updateView(
tableId: string,
viewId: string,
parts: {
name?: string;
description?: string;
filter?: unknown;
sort?: unknown;
group?: unknown;
columnMeta?: unknown;
options?: unknown;
}
): Promise<unknown> {
const base = `/api/table/${tableId}/view/${viewId}`;
let last: unknown;
if (parts.name !== undefined) last = await this.call('PUT', `${base}/name`, { name: parts.name });
if (parts.description !== undefined)
last = await this.call('PUT', `${base}/description`, { description: parts.description });
if (parts.filter !== undefined) last = await this.call('PUT', `${base}/filter`, { filter: parts.filter });
if (parts.sort !== undefined) {
// The API wants {sortObjs:[...]}; accept the natural bare-array form too.
const sort = Array.isArray(parts.sort) ? { sortObjs: parts.sort } : parts.sort;
last = await this.call('PUT', `${base}/sort`, { sort });
}
if (parts.group !== undefined) last = await this.call('PUT', `${base}/group`, { group: parts.group });
if (parts.columnMeta !== undefined)
last = await this.call('PUT', `${base}/column-meta`, parts.columnMeta);
if (parts.options !== undefined)
last = await this.call('PATCH', `${base}/options`, { options: parts.options });
return last;
}
deleteView(viewId: string) {
return this.call<{ id: string }>('DELETE', `/api/view/${viewId}`);
deleteView(tableId: string, viewId: string) {
return this.call<{ id: string }>('DELETE', `/api/table/${tableId}/view/${viewId}`);
}
// ---- view preset (self-built contract) ----
@@ -297,10 +338,6 @@ export class Client {
}
// ---- SQL (read-only) ----
sql(baseId: string, sql: string) {
return this.call<unknown[]>('POST', `/api/base/${baseId}/sql`, { sql });
}
// ---- plugin ----
getPlugins() {
return this.call<unknown[]>('GET', '/api/plugin');