Commit Graph
33 Commits
Author SHA1 Message Date
yeuimu 92e73f7baa fix(automation): fire record triggers on all grid write paths (paste/clear/duplicate)
Investigation of the 'grid edits bypass the REST layer' report found the
ShareDB socket submit path is dead code (adapter db.commit() throws and no
frontend submitOp callsites exist) — grid cell edits already flow through
record.updateCell -> REST updateRecord and DO fire triggers. The real
coverage gaps were the selection-layer writes that call the open-api
SERVICES below the controller where the manual emits live:

- v1 selection paste/paste-by-id, clear/clear-by-id (grid paste, Delete-key
  clear): emitted nothing. Now emit automation.record.create/update with
  full post-write records + changedFieldIds/oldFields diffs (best-effort —
  emission failure logs but never fails the write).
- v1 record duplicate: emitted nothing. Now emits automation.record.create.
- v2 paste endpoints (range paste + paste-stream + paste-by-id-stream):
  returned only counts. PasteResult/PasteStreamDoneEvent now carry per-record
  update diffs and created-record values through to the nest layer, which
  emits create/update events (full record + diff metadata).
- v2 record duplicate: now emits automation.record.create.

Form submissions intentionally remain formSubmitted-only (independent
channel); deletions have no trigger category. Docs corrected: grid cell
edits fire triggers (they are REST), coverage table updated.

Verified live: duplicate / range paste / stream paste on a v2 base each
produced a workflow execution with full triggerInput; selection e2e
114/114, PasteHandler specs 30/30, selection unit 31/31.
2026-08-18 18:54:52 +00:00
yeuimu 7281a4f2e6 docs: workspace packages must be rebuilt before the backend (silent stale-dist trap)
The backend never compiles @teable/* sources — it loads packages/*/dist via
node_modules symlinks (@teable/v2-core is a webpack external, read at runtime
from packages/v2/core/dist/index.cjs). Editing a package's src and only
rebuilding apps/nestjs-backend ships nothing: no error, old behavior online.
Documented the correct build order + verification greps in AGENTS.md and the
skill's development guide (found while shipping the defaultValue cascade fix).
2026-08-18 09:06:24 +00:00
yeuimu 4176165ec7 docs(skill): oldFields is top-level, not nested in record
E2E feedback: script following the documented 'record.oldFields.*'
nesting read undefined. Actual payload shape (verified in
workflow_execution.trigger_input + trigger node output):
triggerInput.oldFields = { fldXXX: value } at TOP level, sibling of
record. Align all trigger output docs (recordUpdated §2, recordCondition
§3, recordCreatedOrUpdated §4) + add explicit access examples
(triggerInput.oldFields?.['fldXXX'], {{n0.oldFields.fldXXX}}).
2026-08-18 08:22:45 +00:00
yeuimu 9ec7dcb15a docs(skill): warn --config must be flat action config
Wrapping --config in {"config":{...}} deploys a silent empty script
(succeeds in ~1ms, result {}). Document the trap + verification recipe
(workflow get --json, nodes[].config.code non-empty).
2026-08-18 07:49:51 +00:00
yeuimu 70f8372fbc fix(workflow): deliver changedFieldIds + oldFields to recordUpdated trigger
Engine gap: the 'automation.record.update' event carries changedFieldIds
(and now oldFields), but RecordUpdatedTrigger.accumulate() dropped both —
scripts had no way to see what changed or pre-update values, despite the
documented trigger output (trigger-types.md).

- record-updated/record-condition/record-created-or-updated triggers:
  pass changedFieldIds + oldFields through to accumulate()
- batch-buffer TriggerEvent += oldFields; batch flush records[] entries
  now carry changedFieldIds + oldFields per record
- v2 updateRecord/updateRecords: emit oldFields (pre-update snapshot)
- v1 controller updateRecord/updateRecords: snapshot before-fields via
  getRecordsById, compute changedFieldIds (deep-equal diff, same
  semantics as v2) instead of hardcoded [] — v1/v2 parity
- new spec record-updated.trigger.spec.ts (4 tests, passthrough +
  watch-filter + table isolation); workflow suite 16 files / 99 tests
- trigger-types.md: document semantics + honest scope (REST API writes;
  ShareDB grid edits do not fire this trigger)

Live-verified: PATCH 流转角色 → trigger_input.changedFieldIds=[fld...],
oldFields=[售前客服] → record=[技术]; script reads oldRole via
triggerInput; fieldIds watch-filter blocks non-watched fields.
2026-08-18 07:49:51 +00:00
yeuimu 80801346b5 fix(workflow): sendNotification skip-on-empty-toUserId + error precision + doc unification
- toUserId 解析为空时静默跳过 (succeeded, sentCount=0, skipped:true) 而非 failed
  → 支持'仅有重复订单时才通知'的条件通知编排
- 必填校验只列出实际缺失字段 (message),不再误报已解析的字段
- 文档统一变量格式: {{nodeId.path}} 为主,{{index.path}} 兼容
- spec 6/6,线上实测: 空收件人 skip + 非空正常发送均验证
2026-08-17 10:11:25 +00:00
yeuimu 146d62c80b docs(skill): document sendNotification action (7 actions total) + CLI support
- skill: SKILL.md (7 actions in description + routing table entry), action-types.md
  (new section #4 sendNotification: config keys, variables, system-in-app semantics),
  automation-guide.md/cli-reference.md (--to-user/--title/--message/--severity/--path),
  development.md (7 actions), metadata.json abstract
- cli: action add --type sendNotification now builds config from granular flags
  (toUserId/title/message/severity/path); ACTION_TYPES includes sendNotification;
  live-verified via workflow create + action add + delete
2026-08-17 07:50:15 +00:00
yeuimu 3a03e0753c fix(cli): read commander's camelCase keys for kebab-case flags
commander stores --code-file as opts.codeFile (kebab keys are never set),
so 'action update --code-file' silently no-opped (workflow.ts read
opts['code-file']) and 'auth token create --base-ids/--full-access'
never applied (auth.ts read the kebab keys). The 'action add' path was
unaffected because buildActionConfig's pick() checks both forms.

- workflow.ts action update: opts['code-file'] ?? opts.codeFile
- auth.ts token create: baseIds/fullAccess dual-form read
- verified live: action update --code-file now persists the new script
  (was silently keeping old code); commander dual-key behavior confirmed
  by direct parse (kebab keys absent, camelCase present, and this
  commander version drops the --base-ids VALUE entirely — the dual read
  covers both)
2026-08-16 17:47:17 +00:00
yeuimu e1a91dfa84 feat(cli+skill): make documented field UX real + fix skill doc drift
CLI (packages/inkreach-cli):
- new src/lib/field-normalize.ts: type aliases (sel/num/text/mul/…),
  select string-choices -> {name}, compact --fields 'Name:type:choices'
  expansion, smart type inference from --options shape / --name
  (--type now optional), name->ID resolution for foreignTableName /
  linkFieldName / lookupFieldName (incl. link-field foreignTableId
  backfill + unknown-key stripping), rating/number/button required-
  option defaults
- field create: +--description/--lookup-options/--is-lookup
- field update --options + table create --fields route through the
  normalizer; DEFAULT_ENDPOINT :3000 -> :3003
- 20 unit tests (field-normalize.spec.ts, vitest); live-verified:
  string choices, alias, foreignTableName, lookup-by-names, compact
  format, inference (priority->singleSelect, max:5->rating)

Skill docs (.agents/skills/inkreach-teable):
- ports :3000 -> :3003 everywhere (backend real port; :3000 is an
  unrelated container on this host)
- remove dead hr_admin account; note throwaway signup + real admin
- plugin register: actual URL-based flags; document plugin csp
  list/set; view/dashboard install requires config enable first
- field-types.md marked verified-2026-08 (aliases/inference/name
  resolution now implemented + tested)
- development.md: webpack dist/index.js cache trap; PGPASSWORD
  placeholder instead of wrong literal
2026-08-16 05:58:01 +00:00
yeuimu 5a984eb5f8 docs(skill): update inkreach-teable skill for fixed field routes + select-choice dependency
- cli-reference.md: field get/update/delete now <tableId> <fieldId>;
  new field visible-choices command; note --options goes via convert API
- tables-fields.md: correct update/delete signatures; options-rewrite
  semantics (convert API, whole-replace not merge); new section on
  select-choice visibilityFilter dependency (read/write/evaluate,
  backend 400 guards, write-time enforcement message)
2026-08-16 05:35:23 +00:00
yeuimu 10b52db8b2 fix(field): enforce select-visibility guard on the V2 convert path + repair inkreach-cli field routes
- FieldOpenApiV2Service.convertField now calls the shared V1 validator
  (assertSelectVisibilityConfigValid) before executing the update command.
  Previously only the V1 path validated self-reference / dependency cycles,
  so invalid configs could be persisted through V2 (verified live: a
  self-referencing choice was accepted with 200).
- V1 assertSelectVisibilityConfigValid private -> public (shared guard).
- Unit tests: 3 new cases in field-open-api-v2.service.spec.ts (guard
  invoked with exact args; rejects before executing when validator throws;
  called with raw type/options for non-select conversions). 63/63 pass;
  tsc build config 0 errors.
- inkreach-cli: field get/update/delete targeted non-existent /api/field/:id
  routes; now use /api/table/:tableId/field/:fieldId. update --options goes
  through the convert API (the PATCH update API strips options by design).
  New: convertField + getVisibleChoices client methods, field visible-choices
  command (evaluates choice visibility for a record).
2026-08-16 05:31:05 +00:00
yeuimu 4dc65c2a0c fix(test): update plugin-base-config spec to match findMany implementation
getEffectiveConfig was rewritten in 948a20b88 to fetch all configs via
findMany (Prisma cannot reliably WHERE on nullable String? with ''), but
the spec still mocked the old findFirst call chain — the table-override
test silently fell back to defaults. Mocks now return the config array.
2026-08-14 07:27:22 +00:00
yeuimu 07836c0165 feat(plugin): dynamic CSP connect-src from plugin manifests
Bundle plugins run inside the base page (not a sandboxed iframe), so their
declared apiOrigins must be in the PAGE's connect-src. Previously the page
CSP was hardcoded static — plugins connecting to new APIs required a code
change + rebuild.

- GET /api/plugin/csp-origins: union of all plugins' manifest apiOrigins
- [[...slug]].tsx getServerSideProps: fetches origins via ssrApi, merges
  with static fallbacks ('self' + 8001 hosts), builds CSP dynamically
- PUT /api/plugin/:pluginId accepts apiOrigins (merged into manifest JSON)
- CLI: plugin csp list / plugin csp set <id> --origins '[...]'
- @DisabledPermission on plugin GET/PUT routes (default-deny guard blocked
  PAT auth); ownership still enforced via manageablePluginCreatedBy()
2026-08-14 05:48:56 +00:00
yeuimu 5ed6569535 merge: plugin CLI commands + base mount + CSP apiOrigins + developer whitelist 2026-08-14 02:50:02 +00:00
yeuimu bc8c76851d docs(skill): add plugin module to SKILL.md + routing rules + description
- Module map: new Plugins row (list/install/uninstall, dev allow/revoke)
- Routing rules: 3 new plugin intents (custom UI, allow devs, external API)
- Description: mention dynamic plugins + dev whitelist + CSP
2026-08-13 15:39:56 +00:00
yeuimu 49836d2655 docs: add plugin CLI commands to skill reference 2026-08-13 15:29:41 +00:00
yeuimu 3eb66513ea feat(plugin): developer whitelist CLI + idempotent backend
- CLI: plugin dev list/allow/revoke (userId or email)
- Backend: PluginDeveloperService.addDeveloper is now idempotent
  (returns existing record instead of unique-constraint 500)
- Backend: removeDeveloper silently no-ops on P2025 (not found)
- Controller: route api/plugin-developers (avoids :pluginId collision),
  @DisabledPermission so admin check is the sole gate
2026-08-13 15:07:32 +00:00
yeuimu 1708e7997a feat: form-submitted trigger multi-form dropdown + skill/docs sync 2026-08-12 06:22:30 +00:00
yeuimu 73998173a8 docs(skill): update formSubmitted trigger docs for multi-form support
Reflect the formId(formId) -> formIds[] change across skill + CLI + docs:
- skill api-reference/trigger-types.md: formIds array config + legacy note
- skill guides (automation-guide.md, cli-reference.md): --form-ids <csv>
- inkreach-cli: add --form-ids option (csv parse), keep --form-id legacy
- docs/automation/triggers.md: P1 formSubmitted marked done
- docs/automation/README.md: formSubmitted config fields updated
2026-08-12 03:59:12 +00:00
yeuimu ef013690f3 feat(workflow): expose trigger user role to script actions
Scripts can now branch on WHO triggered the workflow and their role in the
base, without an extra API call (the workflow token has no role-read scope).

New triggerInput fields (user-initiated triggers only: buttonClick /
recordCreated / recordUpdated / recordCondition / formSubmitted):
- _triggerUserRoles: string[]  (enabled role display names in this base)
- _triggerUserIsAdmin: boolean (space owner / AM admin / AM-disabled)

Absent for scheduledTime / webhook (no user context).

Implementation:
- trigger-user-info.ts: resolveTriggerUserInfo(prisma, baseId, userId) —
  standalone, unit-tested (8 tests). Mirrors the admin logic in
  AuthorityMatrixPermissionService but returns only role names + bool.
- workflow-runner.service.ts: resolve once at first action node, merge into
  enrichedInput (both execute() and test() paths).
- skill action-types.md: document the fields + script branching example.
2026-08-11 10:08:12 +00:00
yeuimu cf25c8ff93 feat(cli): support --dependencies for script actions
- action add/update: new --dependencies <json> option
- parseDependencies accepts JSON, name@version shorthand, or bare names
- update is incremental; --dependencies replaces the whole array
- docs: deployment/systemd.md + skill SKILL/action-types/development
  document the npm-deps feature and the service-PATH requirement
  (spawn npm ENOENT without nvm bin dir on PATH)
2026-08-11 05:51:27 +00:00
yeuimu aff4d70b71 chore(cli): rebuild dist for search[] tuple fix 2026-08-07 18:44:54 +00:00
yeuimu e5975fe5bc docs(skill): add filter operator reference by field type
Add comprehensive operator-to-field-type mapping table covering:
- text/number/boolean/date base operators
- select/link/user field-specific operators (isAnyOf, hasAnyOf, etc.)
- date sub-operators (today, pastWeek, exactDate, etc.)
- conjunction (and/or) + nesting
- Warning: isAnyOf not valid for text fields — use OR conjunction
2026-08-07 18:00:37 +00:00
yeuimu 6527602272 fix(cli): search param uses search[] tuple format
The records API requires search[]=value (tuple), not search=value
(string). Without this, the CLI returned 400 'expected tuple, received
string'. Same fix already exists in the workflow getRecords action.
2026-08-07 17:55:03 +00:00
yeuimu e1d6b03a61 fix: getRecords action page→take/skip + prisma engine fix + CLI fixes
- getRecords automation action: translate page/pageSize to skip/take
  before calling the records API (which ignores page/pageSize, causing
  pagination to always return first 100 records)
- Add postbuild script to restore correct prisma engine binary after
  nest build (which generates an incompatible engine causing SIGSEGV)
- CLI fixes (from earlier commits):
  - deleteRecords: query params instead of JSON body
  - deleteTable: add /base/{baseId} prefix
2026-08-07 16:55:18 +00:00
yeuimu 0f3c88126c fix(cli): table delete uses correct route /api/base/{baseId}/table/{tableId}
CLI was sending DELETE /api/table/{tableId} but the only
controller is at api/base/:baseId/table, causing 404.
2026-08-07 10:55:16 +00:00
yeuimu be21be9465 fix(cli): batch record delete sends recordIds as query params
The API controller reads recordIds from @Query(), not @Body.
CLI was sending { records: ids } as JSON body, causing:
'expected array, received undefined at "recordIds"'

Fixed: rename deleteRecord→deleteRecords, send recordIds as
repeated query params (?recordIds=rec1&recordIds=rec2).
2026-08-07 10:32:25 +00:00
yeuimu 0891bda134 feat(cli): add inkreach-teable plugin commands
plugin list: list registered plugins (id/name/status/runtimeType)
plugin register: register by URL (--name --url --positions)
plugin config list: list base configs (pluginId/tableId/enabled/form/write)
plugin config enable/disable: enable/disable per table or base-wide

Added 5 client methods: getPlugins, registerPlugin, listPluginConfigs,
upsertPluginConfig for CRUD.
2026-08-06 09:03:04 +08:00
yeuimu 584b75ba69 fix(skill): trim inkreach-teable description under 1024-char limit
The skill description was 1033 chars, exceeding pi's 1024-char limit and
causing a 'Skill conflicts: description exceeds 1024 characters' warning,
which prevents the skill from loading/triggering reliably.

Shortened to 990 chars while preserving all trigger-relevant keywords
(inkreach-teable/Teable, ID patterns bseXXX/tblXXX/..., configure
tables/automations, dev/deploy, systemd). Also fixed YAML folded-scalar
artifacts that injected stray spaces after slashes
('/ formSubmitted', '/ httpRequest') by avoiding line breaks after '/'.
2026-08-05 09:22:26 +00:00
yeuimu 3e06a10fe0 skill(iter-1): fix CLI getRecords flag + docs from eval feedback
Ran 4 end-to-end evals (skill-creator flow): all 23 assertions passed (100%).
The evals exposed 3 issues, now fixed:

1. CLI bug: getRecords/createRecord/updateRecord --target-table flag was
   read as opts['target-table'] but commander stores it camelCase
   (opts.targetTable). Added a pick() helper accepting both forms.
2. trigger-types.md: buttonClick output said record.fields.* implying all
   fields; actually record.fields only contains the button field's count.
   Documented the back-channel API fetch pattern for other fields.
3. automation-guide.md: documented that 'workflow test' uses mock input
   with empty fields, so upstream outputs are absent in test mode — use
   real fire for data-dependent validation.

Also: corrected button field options (workflow.id object, not workflowId).
2026-08-05 10:48:33 +08:00
yeuimu d3790ab856 refactor: rename skill 'inkreach-teable-ops' -> 'inkreach-teable'
Rename the skill directory and update all references (SKILL.md frontmatter
name, AGENTS.md, design doc, install.sh). Git tracks this as a rename so
history is preserved.
2026-08-05 10:35:04 +08:00
yeuimu 3810c33d69 refactor: rename CLI command 'inkreach' -> 'inkreach-teable'
Rename the command (and its config namespace) to match the project name:

- bin:          inkreach        -> inkreach-teable
- program name: inkreach        -> inkreach-teable
- config dir:   ~/.inkreach/    -> ~/.inkreach-teable/
- env vars:     INKREACH_*      -> INKREACH_TEABLE_*
- all docs (SKILL.md, guides, api-reference, AGENTS.md, design doc, install.sh)

Unchanged (intentional):
- npm package name: @inkreach/teable-cli
- package directory: packages/inkreach-cli/
- skill name: inkreach-teable-ops
- organization/authors: 'inkreach' (npm scope @inkreach)

Existing config auto-migrated (~/.inkreach -> ~/.inkreach-teable). Verified
end-to-end: auth status, workflow list (636), build clean.
2026-08-05 10:14:26 +08:00
yeuimu 5fa843df05 feat: add inkreach CLI + inkreach-teable-ops skill
A self-built CLI and companion agent skill for operating a deployed
inkreach-teable instance — configuring tables, fields, records, views, and
the project's OWN automation engine.

Why not the official 'teable' CLI:
  inkreach-teable's automation is self-built with a different contract:
  - conditions (not filter), fieldIds (not watchFieldIds)
  - recordCondition (not recordMatchesConditions)
  - webhook route /api/webhook/{baseId}/{wfId} (not the /base//workflow/ form)
  - scheduledTime backend honors only timing.type + timing.interval
  The official CLI targets the upstream contract and 404s/misconfigures here.
  Table/field/record/view APIs ARE upstream-compatible, but automation is not.

inkreach CLI (packages/inkreach-cli/):
  - Node.js + commander, zero extra runtime deps
  - Uses node:http (NOT fetch — Node 24 fetch drops Authorization header)
  - Commands: auth (login/status/token), base/table/field/record/view,
    workflow (list/get/create/update/delete/activate/deactivate/test),
    trigger set (8 types, builds correct config keys), action add/update/delete
    (6 types), secret, runs (list/get/rerun), fire (button-click/webhook), sql
  - --json mode for agent parsing; GET->modify->PUT whole-graph for node edits
  - Verified end-to-end: create workflow -> set webhook trigger -> add script
    action -> activate -> fire webhook -> runs list (succeeded)

inkreach-teable-ops skill (.agents/skills/inkreach-teable-ops/):
  - SKILL.md entry: module map, routing rules, 11 key constraints (the real
    contract keys), execution rules, error recovery
  - guides/: cli-reference, automation-guide, tables-fields, instance-access,
    development (build/restart, known gotchas)
  - api-reference/: trigger-types (8 triggers, exact config), action-types
    (6 actions, sandbox globals), field-types (21 types incl. Button)
  - metadata.json + install.sh
2026-08-05 02:04:39 +08:00