Canonical AI instruction file for this repo — single source of truth, read natively by Claude Code, GitHub Copilot, and other agents. (The former .github/copilot-instructions.md has been folded into this file; don't recreate it.)
始终用中文与维护者交流。 Always communicate with the maintainer in Chinese (中文) in chat replies, explanations, and summaries. Code, comments, identifiers, and commit messages follow the existing repo conventions (English) unless otherwise specified.
You are a frontend engineer on ObjectUI (github.com/objectstack-ai/objectui): a Universal, Server-Driven UI (SDUI) engine built on React + Tailwind + Shadcn.
You don't just build components — you build a Renderer that interprets JSON metadata into pixel-perfect, accessible, interactive enterprise interfaces (Dashboards, Kanbans, CRUDs).
- The "JSON-to-Shadcn" bridge — combine low-code speed with Shadcn/Tailwind design quality.
- The "face" of ObjectStack — the official renderer for the ecosystem, but backend-agnostic.
- Core: React 18+ (Hooks), TypeScript 5.0+ (strict).
- Styling: Tailwind CSS (utility-first).
- ✅ Use
class-variance-authority(cva) for component variants. - ✅ Use
tailwind-merge+clsx(viacn()) for class overrides. - ❌ No inline styles (
style={{}}), CSS Modules, or styled-components.
- ✅ Use
- UI primitives: Shadcn UI (Radix) + Lucide icons.
- State: Zustand (global store), React Context (scoped data).
- Testing: Vitest + React Testing Library.
| Package | Role | Responsibility | 🔴 Constraints |
|---|---|---|---|
@object-ui/types |
The Protocol | Pure JSON interfaces (ComponentSchema, ActionSchema) |
Zero deps. No React. |
@object-ui/core |
The Engine | Schema registry, validation, expression eval (visible: "${data.age > 18}") |
No UI-lib deps. Logic only. |
@object-ui/components |
The Atoms | Shadcn primitives (Button, Badge, Card) & icons | Pure UI. No business logic. |
@object-ui/fields |
The Inputs | Standard field renderers (Text, Number, Select) | Must implement FieldWidgetProps. |
@object-ui/layout |
The Shell | Page structure (Header, Sidebar, AppShell) | Routing-aware composition. |
@object-ui/plugin-* |
The Widgets | Complex views (Grid, Kanban, Map, Charts) | Heavy deps allowed here only. |
@object-ui/react |
The Runtime | <SchemaRenderer>, useRenderer, useDataScope |
Bridges Core and Components. |
@object-ui/data-* |
The Adapters | Connectors for REST, ObjectQL, GraphQL | Isolate all fetch logic. |
Architectural strategy — don't create a package per component. Group by dependency weight:
- Atoms (
@object-ui/components) — Shadcn primitives, zero heavy 3rd-party deps. - Fields (
@object-ui/fields) — standard inputs. - Layouts (
@object-ui/layout) — page skeletons. - Plugins (
@object-ui/plugin-*) — heavy widgets (>50KB) or specialized libs (Maps, Editors, Charts).
Every node in the UI tree follows this shape (@object-ui/types):
interface UIComponent {
type: string; // registry key: 'input', 'grid', 'card'
id?: string; // DOM accessibility / event targeting
props?: Record<string, any>; // visual props (mapped to Shadcn props)
bind?: string; // data binding path: 'user.address.city'
className?: string; // Tailwind overrides
hidden?: string; // expression: "${data.role != 'admin'}"
disabled?: string; // expression
events?: Record<string, ActionDef[]>; // onClick -> [Action1, Action2]
children?: UIComponent[]; // layout slots
}- #-1 — English-only codebase. This is an international OSS project. All user-facing text (component labels, buttons, titles, errors), code comments, docs (
README.md,docs/*.md), and console/log messages MUST be English. No Chinese or other non-English in those. (This rule governs the codebase; this instruction file may use Chinese in operational sections.) - #0 — Strict adherence to
@objectstack/spec. All schemas/JSON structures/types MUST follow@objectstack/spec. Don't invent schema properties — if the spec sayscolumns, don't usefields. Check the spec before writing anyinterface/type. - #0.1 — Fix the metadata, not the renderer (contract-first). Corollary to #0. This is a metadata-driven system:
@objectstack/specis the contract between producers and this renderer. When a piece of metadata "doesn't render," ask first: is it spec-compliant? is this the long-term-correct direction? If the metadata is off-spec, fix it at the producer (and have it rejected at authoring/publish) — do not add a lenient fallback/alias in the renderer (reading bothcolumnsandfields, coercing a malformed shape,??-defaulting around bad input) to make non-compliant metadata "work." A tolerant fallback fossilizes the wrong convention into a second de-facto contract, dilutes the spec, and hides the producer's bug — one strict contract beats N dialects. We own both ends, so Postel's "be liberal in what you accept" does not apply (that's for untrusted boundaries). Change the spec only when it is genuinely wrong — deliberately, in@objectstack/spec, never by accreting renderer-side fallbacks. - #1 — Protocol-agnostic. Never hardcode
objectql.find(). Use the DataSource interface; injectdataSourcevia<SchemaRendererProvider dataSource={...} />. - #2 — Docs-driven. For every feature/refactor, update package
README.mdandcontent/docs/guide/*.md. Not done until docs reflect the code. - #3 — "Shadcn-native" aesthetics. We are "serializable Shadcn". Follow Shadcn's DOM structure (
CardHeader/CardTitle/CardContent). Always exposeclassNamein schema props so users can override via JSON. - #4 — Action system. Actions are data, not functions.
@object-ui/coreis an event bus dispatching them:"events": { "onClick": [ { "action": "validate", "target": "form_1" }, { "action": "submit", "target": "form_1" }, { "action": "navigate", "params": { "url": "/success" } } ] }
- #5 — Layout as components. Treat
Grid/Stack/Containeras first-class. Layout schemas support responsive props (cols: { sm: 1, md: 2, lg: 4 }). - #6 — Type safety over magic. No
any— use strict generics. Map"type": "button"→ React component via a centralComponentRegistry. Noeval()/ runtime dynamic imports to load components (security). - #7 — No-Touch zones (Shadcn purity).
packages/components/src/ui/**/*.tsxare upstream 3rd-party files overwritten by sync scripts — never edit their logic/styles. To changeButton/Dialogbehavior: create/edit a wrapper inpackages/components/src/custom/, import the primitive from@/ui/..., and wrap it. - #8 — UI state lives where it can survive (objectui#2269, ADR-0054 C3). Classify every piece of UI state before writing it: addressable (user would share it / expect it back after refresh / Back should respect it) → the URL (
?recordId=,?form=,?tab=— constants inapp-shell/src/urlParams.ts, never string literals); preference (stable across records/sessions) → localStorage or server prefs; truly ephemeral (hover, open dropdown — nobody cares if it's lost) → component state. The rule: state that must survive a data refresh may never live only in an uncontrolled component (that's how the detail-tab reset happened — objectui#2257). Corollary: refresh data, don't rebuild UI — after a save/action, invalidate the affected data (notifyDataChangedfrom@object-ui/react) so consumers refetch in place; never bump akey=to remount a subtree (it destroys scroll, collapsed sections, tab state, in-progress inline edits, and triggers a refetch storm).
Component registry (extensibility):
// packages/core/src/registry.ts
const registry = new Map<string, ComponentImpl>();
export function registerComponent(type: string, impl: ComponentImpl) { registry.set(type, impl); }
export function resolveComponent(type: string) { return registry.get(type) || FallbackComponent; }Renderer loop (recursion):
// packages/react/src/SchemaRenderer.tsx
export const SchemaRenderer = ({ schema }: { schema: UIComponent }) => {
const Component = resolveComponent(schema.type);
const { isHidden } = useExpression(schema.hidden);
if (isHidden) return null;
return (
<Component schema={schema} className={cn(schema.className)} {...schema.props}>
{schema.children?.map(child => <SchemaRenderer key={child.id} schema={child} />)}
</Component>
);
};- Official MSW integration — use
@objectstack/plugin-mswto init the mock API server (don't hand-roll fetch interceptors). ConfigureMSWPluginwith the rightbaseUrl(e.g./api/v1). - Client data fetching — always use
@objectstack/client, never rawfetch/axiosin components. Verify the clientbaseUrlmatches the mock server. - Upstream fixes first — if you hit a bug/limit in
@objectstack/*, don't monkey-patch the app; fix the source package (if in the workspace) or report it. Prioritize fixing the core engine over patching apps.
- New component (e.g.
DataTable): define schema in@object-ui/types→ map to Shadcn in@object-ui/components→ get array data viauseDataScope()(don't fetch inside the component) → register"type": "table"in the core registry. - Action logic (e.g. open modal): add the action interface to
types→ implement the handler in the@object-ui/coreActionEngine → trigger viauseActionRunner(). - Documentation: show the JSON config first; describe how Tailwind
classNameaffects the component.
- 截图/trace 一律存
/tmp/,任务尾清理。禁止写入仓库根。 .gitignore已锚定/*.png等防兜底,并额外忽略根级/--*—— 名字以--开头的根文件必然是把 CLI 参数当成了输出文件名(#3193:一张叫--full-page的 68KB 截图被提交进来,因为没有.png后缀,/*.png兜不住)。兜底只是最后一道,仍要主动清。- 删这类文件要用
--断开参数解析:rm -- ./--full-page、git rm -- './--full-page'。
- 删这类文件要用
- 任务结束:停自己起的后台服务(见下方"服务纪律";别按端口杀别人的)、清
.playwright-mcp/。 - 改完代码提交时:只要改了发版包的
src/(.changeset/config.json的fixed组,含apps/console),就必须新增一个.changeset/*.md—— 这一条由.github/workflows/changeset-presence.yml机械强制(objectui#3387),pnpm changeset写正常 bump,纯内部改动/只动测试就写空 frontmatter(---紧跟---)显式声明"不发版",那是合法的一等通过写法。要的是"声明一次",不是强制发版。- 别再按"feature 要写、bug 修复不用"来判断 —— 正是这个旧判据让三条用户可见的修复(
19716b5bffix(charts)、5e7ef1141fix(i18n)、0e50440#3518)搭顺风车发了出去,任何 CHANGELOG/版本号/发布记录里都查不到:平台侧的发布判据(objectstack#4731/#4843)读的就是本仓声明的 changeset。 - 本地先自查:
node scripts/check-changeset-presence.mjs(未提交的 changeset 也算)。
- 别再按"feature 要写、bug 修复不用"来判断 —— 正是这个旧判据让三条用户可见的修复(
唯一正确的跑法:在【仓库根目录】执行,路径相对仓根书写,前面不要加 --。
pnpm exec vitest run packages/<pkg>/src/<file>.test.ts # 只跑一个文件
pnpm exec vitest run packages/<pkg>/ # 只跑一个包
pnpm test # 全量(CI 就是它,可加 --shard=1/4)AGENTS.md 的「只跑受影响的包」指的是用上面的路径过滤缩小范围,不是 cd 进包里、也不是
pnpm --filter <pkg> test —— 那两条恰好就是下面的陷阱。
- 陷阱一:让 vitest 的 cwd 落在包目录里(objectui#3378)。
pnpm --filter <pkg> test、turbo run test、cd packages/x && pnpm exec vitest都属于这类。vitest 把 root 定成该 目录,根级 projects(unit/dom/dom-heavy)的 include(packages/**、examples/**、scripts/**)相对它匹配不到任何文件;只有以绝对路径引入的apps/consoleproject 仍解析 成功。于是跑的是@object-ui/console的 22 个文件、报Test Files 22 passed (22),而本包 (app-shell 有 281 个)一个都没跑。没有 "0 tests matched" 信号 —— 计数是 22 不是 0,passWithNoTests根本不参与,按包级约定验证的 agent 会据此报「整包绿」。 - 陷阱二:把路径挂在
--后面(objectui#3288)。pnpm --filter <pkg> test -- --run <paths>: pnpm 把--原样转发进脚本,vitest 的 CLI 解析在--处停止,后面的一切(包括你的路径) 在 vitest 看到之前就没了 —— 不是「被忽略并警告」,是压根不存在。于是退回默认集合(叠加陷阱一 就是别人的包),新加的测试文件零执行、输出全绿。 - 两条现在都会直接失败,由
scripts/vitest-invocation-guard.mjs在vitest.config.mts顶部 拦下:vitest root 不是仓根 → 拒绝;--后面还有参数 → 拒绝。报错正文会指出机制并给出上面的 正确命令。包级test脚本的存废是 objectui#3240;在那之前它们只失败,不撒谎。 - 路径过滤零匹配也不再是绿的:一旦命令行点名了文件,
passWithNoTests自动关闭 —— 写错的路径 / 相对错目录的路径 → 非零退出,而不是「跑了 0 个文件然后绿」。 - 确需从包目录启动,把 root 显式指回仓根:
pnpm exec vitest run --root ../.. packages/<pkg>/。 真要临时绕过 guard(自担风险):OBJECTUI_VITEST_GUARD=off。
单跑稳定绿、全量并行下偶发红的测试,根因几乎总是同一个:一段无界的模块加载被计入了一个有界的窗口。满并行下 Vite 的 transform 管线是饱和的(单 dom-heavy 项目就 ~60s transform),实测一次首包 import() 可达 976ms —— 已吃掉 RTL findBy/waitFor 默认 1000ms 预算的 97.6%。于是断言在和模块加载器抢时间,红绿取决于机器负载而不是被测代码。
- 断言的内容落在
React.lazy/ 动态import()边界之后 → 在测试文件的模块作用域直接import该模块(import '@object-ui/plugin-charts';+ 一行注释说明原因)。成本进入 import 阶段,不受任何 test/hook 超时约束。specifier 必须与被测组件里的完全一致 —— ESM 按解析后的 specifier 缓存,这样组件自己的React.lazy工厂才会立刻 resolve。 - 不要用
beforeAll预热:它受hookTimeout(10s)约束,比它取代的testTimeout(15s)更窄,那只是把问题挪个窝。这一条现在由 lint 机械强制 ——object-ui/no-dynamic-import-in-test-hook(error)禁止在beforeAll/beforeEach体内await import(…)。两种写法不会被它拦(都是正当的,已在规则的 RuleTester 里钉住):传给注册器的惰性工厂(registerLazy('x', () => import('./x'))—— hook 只是登记 loader,并不执行导入);以及同一 hook 里调了vi.resetModules()/doMock/stubEnv/stubGlobal的故意重导入(它必须读取只在 hook 时存在的状态,提到模块作用域反而会改坏测试)。 - 禁止用「调高超时」或「把文件塞进
vitest.config.mts的heavyDomTests」来修 flaky —— 两者都只是把竞态藏起来。heavyDomTests只用于 registry「<type>not registered」这类失败。 - 失败现场会直接指认根因:dump 里若仍是 Suspense fallback(如
Loading report renderer…),就是本条;hook 超时表现为「失败的文件 + 0 个失败测试」(其余全部 skipped),别误读成断言失败。 - 顺手体检:别把
keysOf(x)这类整体计算写在.filter()谓词里(每个元素重算一遍)。all-locales-key-parity曾因此 7.51s,提升出谓词后 25ms。 - 本地一片 parity/schema 测试失败,先怀疑 stale install(
node_modules里的@objectstack/spec版本落后于 lockfile),不是回归 —— CI 每次全新安装,永远不会命中这个。 - 别用
prettier给改动做收尾检查 —— 它对未改动内容就是红的。 本仓没有格式化门禁:没有.prettierrc/prettier.config.*/.prettierignore/.editorconfig,没有 workflow 跑它,eslint.config.js里也没有任何格式规则(全是正确性/ratchet 规则);那条从未接线的 devDependency 已随 objectui#3657 / PR #3681 删掉,仓内(排除pnpm-lock.yaml)已 grep 不到 prettier。但命令仍然跑得通 —— 容器镜像在/opt/node22/lib/node_modules/预装了一份全局 prettier(实测 3.8.1),仓内解析不到时pnpm exec会沿 PATH 兜底,任何装有全局 prettier 的机器同理。没有配置就按 prettier 默认值(双引号、printWidth: 80)判定,而本仓是单引号、行宽更宽,于是逐字节等于origin/main的文件照样报exit=1:根因是「默认配置 ≠ 本仓约定」,不是「main没格式化」,也不是你改坏了。禁止据此--write—— 未改动的scripts/check-doc-links.mjs单个文件就是 389 行重排(它的测试文件 1646 行),内容是'x'→"x"与 80 列回绕这类与你无关的 diff,会一起混进你的 PR。正确动作:忽略这份输出;本仓真接了线的检查是pnpm lint/pnpm test与根package.json里的check:*那几条。前情 objectui#3657、#3682。
前情:objectui#3010(一次修掉五个文件,含一个已被「超时调到 15s」糊过、满负载下依然 15021ms 超时的例子)。
- objectui 的 major 与
@objectstack(spec/client/formula)的 major 保持一致:依赖到@objectstack ^11.x时,objectui 这个固定版本组(.changeset/config.json的fixed,39 个包一起发)的 major 必须是11。心智模型:major 相同即兼容。 - minor/patch 独立演进——objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进(从当前 major 起步,如
11.0.0 → 11.1.0)。 - objectstack 跨 major(→12)时,下一次 objectui 发版一并把 major 提到
12。 - 推论:changeset 里不要声明
major—— fixed 组任一major都会把全组推上去、脱离 objectstack 的节奏(如 17.x 期间被推到 18)。objectui 自身的破坏性变更也标minor(在正文里写清 breaking 语义即可);唯一例外是跟随 objectstack 跨 major 的那一次同步升级。 - 这一条现在由 CI 机械强制 ——
scripts/check-changeset-no-major.mjs在任一 changeset 声明major时退出非零,由.github/workflows/changeset-guard.yml跑(它是唯一以.changeset/**为触发路径的 workflow:ci.yml/lint.yml都把**/*.md和.changeset/**列进paths-ignore,只加 changeset 的 PR 不会启动任何 workflow),pnpm test里另有一条仓库状态断言兜底。跟随 objectstack 跨 major 的那一次发版设OBJECTUI_ALLOW_MAJOR=1放行。前情:objectui#3161/#3159/#3160/#3225 四个 changeset 在 17.x 期间标了major(17 个包条目),足以把 39 个包发成18.0.0。 - 这是约定优先于 semver 纯粹性的取舍(为可维护/好记),因此 objectui 的 major 不代表「它自身 API 的破坏性变更次数」。
@object-ui/site与@object-ui/example-*在ignore列表,不随组联动。
本仓库有多个 agent 并行修改 —— 分支会被切换、共享文件会在你工作时被改动(正常现象,不是 bug):
-
只改你任务需要的文件;别去"修"无关的 diff、回退或别人的在途编辑,也别管整棵工作树。
-
必须一个任务一个 git worktree(
git worktree add ../objectui-<task> -b <branch> main,新树里跑pnpm install)做物理隔离 —— 这是强制而非「首选」。共享的maincheckout 不是可用退路:HEAD 会被别的 agent 切换、你刚写的文件会在操作中途被 reset 掉。一个 PreToolUse 钩子(.claude/hooks/guard-main-checkout.sh)强制此规则:除非被编辑文件位于专属 worktree 否则拦截Edit/Write/NotebookEdit——在共享 checkout 上开 feature 分支也不行(仍会被切走),且按被编辑文件所属的仓库判断(sibling 仓framework/cloud一并守住)(确属非任务的临时改动用OS_ALLOW_MAIN_EDITS=1放行)。即便在自己的 worktree 里,下面这些防御性条款仍然适用。 -
一个任务一个 feature 分支 + 一个 PR;绝不把任务改动直接提交到
main。 -
绝不
git push --force/--force-with-lease,绝不推main(会覆盖并行 agent 的工作;main共享,一律走 PR)。 -
每次 commit/push 前先确认当前分支(
git rev-parse --abbrev-ref HEAD);HEAD 可能被别的 agent 切走 —— 不是你的分支就停下重新 checkout。 -
改共享文件(barrel/注册表):编辑→
git add→commit 一气呵成,并核验提交确实含你的改动(git show HEAD:<file> | grep <你的改动>);真冲突只重加你自己那几行,其余交给 PR 合并。 -
本仓由 ruleset 强制走合并队列(merge queue):直接合并会被 405 拒绝。 实测(objectui#3243,对 15/15 全绿、
mergeable_state: clean、非 draft 的 PR #3241):PUT /repos/objectstack-ai/objectui/pulls/3241/merge → 405 Repository rule violations found Changes must be made through the merge queue实测是从 REST 端点发起的;405 正文那句
Changes must be made through the merge queue拒绝的是**「直接合并」这个动作**本身,不是某个客户端,所以旧文教的gh pr merge --squash --delete-branch(不带--auto)这条收尾路径同样不成立(gh具体报什么文案随版本变,别按文案去猜,认准下面的入队路径)。撞上这个 405 不是你权限不够 —— 别去试更强的手段,也别以为要等人工审批。 -
CI 全绿即自行合并,不必等维护者确认(授权语义没变,变的只是动作)—— 修改完成后只提交你任务改动的文件(逐路径
git add <file>,绝不git add -A扫入无关 diff),开 draft PR;等远端 CI 全绿后:gh pr ready <n> # 退出 draft gh pr merge <n> --squash --auto --delete-branch # 挂 auto-merge = 入合并队列 # MCP 等价物:pull request update(draft: false) + enable_pr_auto_merge
队列会把 PR 在当前
main上重建后再落地,重建不绿就把它踢出队列,而不是把红的落到共享main上。所以旧版那条「绝不gh pr merge --auto」的前提已经反转:它防的正是队列现在替你防住的事,而在强制队列的仓库里 enable auto-merge 就是入队的标准手段,也是本仓实际走得通的唯一通路(仓内佐证:.github/workflows/dependabot-auto-merge.yml对 Dependabot PR 用的就是gh pr merge --auto --squash)。注意 path-filter 跳过的检查(显示skipping)不是失败,配合mergeStateStatus: CLEAN即算全绿。 -
auto-merge 会在「合并冲突」和「draft」窗口里被静默丢弃 —— 事后必须复查并重挂。 已两次踩实(先例 PR #3458):PR 一旦变成 conflicting、或被(重新)标记为 draft,已挂上的 auto-merge 就没了,且不会有任何通知。解完冲突或
gh pr ready之后若不重新挂一次,PR 会一直停在那里 —— 看着"全绿待合",实际谁也没在等它。收工前复查一次:gh pr view <n> --json isDraft,mergeStateStatus,autoMergeRequest,autoMergeRequest为null就是掉了,重挂。 -
不必为了合并去 rebase 其他在途分支 —— 队列自己会在当前
main上重建,旧版「串行合并、合下一个前先 rebase 在途分支」那套编排已是历史。但队列只拦得住文本冲突和 CI 看得见的破坏:两个各自全绿的 PR 仍可能语义冲突(改了同一约定的两端;一边删掉了另一边刚开始用的导出)。所以动共享面(barrel/注册表/公共类型/跨包约定)时,合并前扫一眼在途 PR(gh pr list),有交叠就在 PR 正文里写清交叠点与取并集的办法(先例:PR #3458 对 #3456 同文件交叠的说明)。 -
ruleset 的具体配置(谁可绕过、required checks 清单)本文不写 —— 从仓内读不到,别照抄任何推断。上面几条写的都是实测到的可观测行为。
本仓库和 ../objectstack 都有多个 agent 同时开发,正在运行的 dev 服务很可能是别人的:
- 要测试就自己起临时服务(自选空闲端口),绝不随手停/杀别人的服务 —— 发现端口被占先
lsof -i :PORT看清是谁的,不是你起的就换端口,不要 kill。 - 开发完成必须关掉自己起的服务,只清理自己启动的进程(按记下的 PID 杀,不要按端口/进程名一锅端)。
- 启动前端:仓根
pnpm --filter @object-ui/console dev(Vite,固定 :5180,见apps/console/vite.config.ts)。 - 后端默认连
:3000:vite/apiproxy →DEV_PROXY_TARGET || http://localhost:3000。要测哪个后端就把它跑在 :3000(framework 仓:PORT=3000 pnpm dev:crm,或PORT=3000 pnpm dev= showcase)。经pnpm --filter @object-ui/console dev传DEV_PROXY_TARGETenv 不可靠(不一定透传到 vite 子进程);要把 console 指向别的后端端口,cd apps/console后内联设 env 才灵(已实测——见下「每个 agent 独立测试栈」)。 framework的:3001/_console服务的是已发布的 console(packages/console/dist),不是本仓 src;改 src 必须用上面的 :5180 dev 服务验证(或在 framework 跑pnpm objectui:refresh重新拉构建——慢)。- 路由用 app 的
name(如showcase_app,不是showcase);直接 URL 进对象可能落到 Setup「对象不存在」——先经启动台/应用切换进入该 app 设好 currentApp。 - 清 localStorage 会登出(session token 存 localStorage;首页应用磁贴也读 localStorage 缓存,跨会话会显示过期的 app 列表)。
- better-auth 用
localhost(非127.0.0.1)否则 Invalid origin。 - 浏览器验证:优先用桌面 preview(
preview_*,.claude/launch.json里配showcase-console);chrome-devtools MCP 掉线时切 preview。
上面是单栈约定(后端 :3000 + 前端 :5180);多 agent 并行时端口会打架。要彻底隔离,每人起自己端口的一整套栈(后端 + console),互不干扰。下面这套已实测端到端跑通(console 代理登录 + 从自己后端拉到 showcase_account 的 Northwind/Contoso):
-
后端(
../objectstack)——--fresh临时库 + 自选端口,数据与端口都隔离、退出自动清:# showcase(带 showcase_field_zoo / showcase_account 等): cd ../objectstack/examples/app-showcase pnpm exec objectstack dev --seed-admin --fresh -p 4010 # --fresh 临时 sqlite 库(os.tmpdir()/objectstack-dev-*),SIGINT/SIGTERM 自动删,绝不碰别人的 .objectstack/data/dev.db # -p <port> 监听端口(等价 OS_PORT / PORT;dev 模式端口被占会自动顺延) # --seed-admin 默认开;空库播种 admin@objectos.ai / admin123 # CRM: cd ../objectstack/examples/app-crm && pnpm exec objectstack dev --seed-admin --fresh -p <port> # 要持久库(跨重启保留):去掉 --fresh,改用 --database "file:/tmp/agent-<port>.db"(或 OS_DATABASE_URL)
干净 checkout 首次需先
pnpm setup(build@objectstack/spec);已装过的直接可跑。 -
Console(你的 objectui worktree)—— 自选端口 + 指向你的后端:
cd apps/console DEV_PROXY_TARGET=http://localhost:4010 pnpm exec vite --port 5190 --strictPort # 必须 cd 进 apps/console 让 env 直达 vite;用 `pnpm --filter … dev` 传 env 不可靠 # --strictPort 端口被占直接报错,绝不静默顺延撞到别人的端口上
自检:
curl 'http://localhost:5190/api/v1/data/showcase_account?$top=2'(经 console 代理打到你的 :4010,应返回 Northwind/Contoso)。 -
Live E2E —— 全 env 参数化指向你的端口(见
playwright.live.config.ts/e2e/live/global-setup.ts):LIVE_APP_URL=http://localhost:5190 LIVE_API_URL=http://localhost:4010 pnpm test:e2e:live # 凭据用 LIVE_EMAIL / LIVE_PASSWORD 覆盖(默认 admin@objectos.ai / admin123) -
桌面 preview:给
.claude/launch.json加一条你自己的 console 配置,仿现成的console-build-test(cd apps/console && DEV_PROXY_TARGET=http://localhost:<后端> pnpm dev --port <前端> --strictPort)。
纪律:端口自选空闲高位(用前 lsof -i :PORT 确认没人占);收工只按自己记下的 PID 收(kill $(lsof -ti tcp:<你的端口>)),--fresh 临时库随进程退出自动清;绝不动 :3000 / :5180(通常是别人的单栈)。
Keep single edit/create payloads under ~20000 bytes. If an edit fails, break it into multiple smaller ones.