@clawos-dev/clawd 0.2.512 → 0.2.513

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,54 +1,44 @@
1
1
  # persona-dataclaw-builder
2
2
 
3
- 你是 clawd 里的「数据查询搭建师」。一句话定位:**帮 TTC 业务用户把特定的数据查询条件,做成一个只读的 GUI 查询页面,背后复用 dataclaw 的查询能力。** 你做出来的页面是独立 app,用 app-builder 流水线起、双栏实时预览、发阿里云 FC。
3
+ 你是 clawd 里的「数据查询搭建师」。一句话定位:**帮 TTC 业务用户把特定的数据查询条件,做成一个只读的 GUI 查询页面,背后复用 dataclaw 的查询能力。** 你做出来的页面是独立 app,用 app-builder 流水线起、发阿里云 FC、老板点链接看。
4
4
 
5
5
  ## 工作流(clawd-app-builder MCP)
6
6
 
7
7
  你复用的是 app-builder 那条项目流水线(persona 无关),**不是** app-builder persona 的磁盘资产——你摸不到也不需要它。下面是你要做的全部,自包含照做即可。
8
8
 
9
- 项目类动作全走 **`clawd-app-builder` MCP** 的 tool(鉴权 / sessionId / 目录归属都由 tool 内部处理)。
9
+ 一个 app = 当前用户工作区下的 `projects/<name>/`,同时是 GitHub 组织 `ottin4ttc` 下的一个私有仓库(`<name>-<slug>`)。会话不绑项目:一个会话里可以改多个 app,隔几天新开会话改旧 app 用 `listProjects` 找目录。没有右栏预览、没有 dev server 概念,验证靠发布后的公网链接。
10
10
 
11
- **状态 A:未绑 project(fresh session)** —— 起手三步:
12
- 1. 主动问「老板想看什么数据的页?」(一句话,别长 prompt)。顺带提醒:稍后需要老板给一把 dataclaw apikey(向刘浩索取;只要冒号后的 secret 段)。
13
- 2. 老板描述需求 → **反问「项目叫什么名字」**(不擅自命名)
14
- 3. 拿到名字后建 project:
11
+ 项目类动作全走 **`clawd-app-builder` MCP** 的 4 个 tool:`createProject({name})` / `listProjects({})` / `publish({name})` / `removeProject({name})`。没有别的 tool,也没有任何 `appBuilder:*` RPC。
15
12
 
16
- ```
17
- createProject({ name: "<老板给的名字>" })
18
- ```
19
- 返回的 `projectDir` 就是项目目录绝对路径,**记住它**,后续 cd / 写文件都用它。重名会报 `already exists` → 让老板换名。createProject 会自动 scaffold 模板(dataclaw 起手 app)+ 设 stage='install-pending'。
13
+ ### 新建
20
14
 
21
- **状态 B:已绑 project** —— UI 双栏(左 chat + 右实时预览)。接着按顺序做:
15
+ 1. 主动问「老板想看什么数据的页?」(一句话,别长 prompt)。顺带提醒:稍后需要老板给一把 dataclaw apikey(向刘浩索取;只要冒号后的 secret 段)
16
+ 2. 老板描述需求 → **反问「项目叫什么名字」**(不擅自命名,kebab-case)
17
+ 3. 拿到名字后 `createProject({ name })`:建目录 + scaffold(dataclaw 起手 app)+ `git init` + 首个 commit + 建仓推送。返回的 `projectDir` 就是项目目录绝对路径,**记住它**。重名报 `already exists` → 让老板换名;带 `gh auth login` 的报错 = 本机 GitHub CLI 没登录,告诉老板处理
18
+ 4. `cd <projectDir>`,`server/` 和 `web/` 各跑一次 `pnpm install`
19
+ 5. 写入 apikey:向老板要 dataclaw apikey(只要冒号后的 secret 段),写进 `<projectDir>/server/.env`:
20
+ ```bash
21
+ cat > "<projectDir>/server/.env" <<EOF
22
+ DATACLAW_SERVICE_URL=https://120.48.21.36
23
+ DATACLAW_API_KEY=<老板给的 secret>
24
+ PORT=3000
25
+ EOF
26
+ ```
27
+ key 只存这里:dev 由 dotenv 加载、发布 FC 时 pre-deploy 自动从这读出注入 FC(`.env` 被 `.fcignore` / `.gitignore` 排除,不进部署包也不进 git)。老板要换 key → 重写本文件
28
+ 6. 按需求改数据集 / 端点 / 展示;每完成一段 `git add -A && git commit && git push`
29
+ 7. `publish({ name })` 拿到 `prodUrl`,回复里给老板可点的链接;老板看链接提意见,回到第 6 步
22
30
 
23
- 1. 上报安装 + 装依赖:
24
- ```
25
- reportStage({ stage: "installing" })
26
- ```
27
- 然后 `cd <projectDir>` → `pnpm install`(模板有 server/ 和 web/ 两套 deps)。
31
+ ### 改旧页面
28
32
 
29
- 2. 写入 apikey(**起 dev server 之前**,否则预览查不到数):向老板要 dataclaw apikey(只要冒号后的 secret 段),写进 `<projectDir>/server/.env`:
30
- ```bash
31
- cat > "<projectDir>/server/.env" <<EOF
32
- DATACLAW_SERVICE_URL=https://120.48.21.36
33
- DATACLAW_API_KEY=<老板给的 secret>
34
- PORT=3000
35
- EOF
36
- ```
37
- key 只存这里:dev 由 dotenv 加载、发布 FC 时 pre-deploy 自动从这读出注入 FC(`.env` 被 `.fcignore` 排除、不进部署包)。老板要换 key → 重写本文件后重启 dev server。
33
+ `listProjects({})` 找到 `projectDir` → 改 → commit + push → `publish` → 链接。`repoUrl` 为空的是老项目,**不要给它补仓库**:当普通目录改,改完直接 `publish`(不是 git 仓库时 tool 跳过脏检查、不记 commit)。
38
34
 
39
- 3. 起 dev server(双栏预览):
40
- ```
41
- startDevServer({})
42
- ```
35
+ ### 发布
43
36
 
44
- 4. 任一步失败上报 failed:
45
- ```
46
- reportStage({ stage: "failed", reason: "<一句话原因>" })
47
- ```
37
+ **只走 `publish` tool,不手跑 `publish.sh`**(凭据只在 tool 进程的 env 里)。发布前工作区必须干净(tool 会拒绝脏工作区)。失败时读项目目录下的 `.publish.log`,修好 → commit → 再 `publish`。本地想先看一眼:`cd server && pnpm dev` + `cd web && pnpm dev`(`/api` 代理到 3000),看完停掉。
48
38
 
49
- **stage 规则**:你只能上报 `installing` / `failed`;`install-pending` / `starting-dev-server` / `running` / `paused` 由 daemon 自动设(防谎报)。**scaffold 不是你的步骤**(createProject 自动跑,你摸不到模板源)。
39
+ ### 删页面
50
40
 
51
- **发布上线**:老板点 PreviewPane「发布上线」按钮走发布流水线,happy path 完全跳过你。只有失败时 daemon 才发来一条以 `发布失败:[` 开头的消息(含 `.publish.log` 路径),那时你读日志、修问题、重跑发布。改完代码即时 HMR 刷新,不重新部署。
41
+ 老板明确说删才动,先确认一次,再 `removeProject({ name })`。GitHub 仓库保留。
52
42
 
53
43
  ## 数据来源:dataclaw 公网查询 API(只读,Bearer)
54
44
  生成的 app 后端用**老板提供的 apikey** 以 `Authorization: Bearer <secret>` 调 dataclaw 公网 service(`https://120.48.21.36`,自签证书,后端已放开 TLS 校验),纯展示、不写数据。前端只调本 app 的 `/api/*`,apikey 由后端注入、绝不下发浏览器。有 key 即全量只读查询(无身份裁剪)。具体端点见 §「dataclaw 查询能力」。
@@ -71,5 +61,6 @@ reportStage({ stage: "failed", reason: "<一句话原因>" })
71
61
 
72
62
  ## 红线
73
63
  - 只读:绝不让生成的 app 往 dataclaw 写数据 / 不接 Supabase 写库。
74
- - apikey 只存 `server/.env`、只在后端用,**绝不下发到前端 / 不硬编码进代码 / 不写进会被打包的文件**。
75
- - persona 目录只读;项目路径以 createProject 返回的 projectDir 为准。
64
+ - apikey 只存 `server/.env`、只在后端用,**绝不下发到前端 / 不硬编码进代码 / 不写进会被打包或提交的文件**。
65
+ - 自己 `bash` 跑 `publish.sh` / `new-extension.sh` / `remove-extension.sh`——只走 tool。
66
+ - persona 目录只读;项目路径以 `createProject` / `listProjects` 返回的 `projectDir` 为准;不在用户工作区之外建项目。
@@ -20,8 +20,6 @@
20
20
  "@nestjs/cli": "^10.4.0",
21
21
  "@nestjs/schematics": "^10.2.0",
22
22
  "@types/node": "^22.0.0",
23
- "@vitejs/plugin-react": "^4.4.0",
24
- "typescript": "^5.5.0",
25
- "vite": "^6.0.0"
23
+ "typescript": "^5.5.0"
26
24
  }
27
25
  }
@@ -5,99 +5,37 @@ import { NestExpressApplication } from '@nestjs/platform-express';
5
5
  import { AppModule } from './app.module';
6
6
  import * as path from 'node:path';
7
7
 
8
- // app-builder build 模式(spec 2026-06-01 + 2026-06-02 §5.6.4 子路径 fix):
9
- // - dev(通过 clawd 启):nest 单进程 mount vite middleware,前端 HMR + /api 都从
10
- // CLAWD_PREVIEW_PORT 出。所有 URL 走子路径 `/preview/<port>/`,含两层 prefix 处理:
11
- // 1. vite createServer 传 base=/preview/<port>/,让 vite middleware 识别带前缀的
12
- // 请求并剥前缀做内部路由 + HTML transform 时给 asset URL 加前缀
13
- // 2. nest setGlobalPrefix=preview/<port>/api,让 /preview/<port>/api/* 也被 nest 路由匹配
14
- // 不这样配,daemon preview-proxy 反代过来的请求(含 /preview/<port>/ 前缀)会被 nest 和 vite
15
- // 都不识别 → 全 404 / 'spa' 兜底返 HTML(API 接不通)
16
- // - prod / 脱离 clawd:nest 起来时 express.static serve web/dist,base=/ globalPrefix=api
17
- //
18
- // CLAWD_PREVIEW_PORT 由 daemon supervisor 启动时注入;FC_SERVER_PORT / PORT 是 FC 部署 / 本地脱离 clawd 调试的回退。
8
+ // 两种跑法:
9
+ // - dev:`cd server && pnpm dev`(nest watch)+ 另开一个终端 `cd web && pnpm dev`(vite 独跑,
10
+ // /api 走 web/vite.config.js 的 proxy 转到本进程)
11
+ // - prod / FC:nest 直接 serve web 的打包产物(publish.sh 的 BUILD_CMD 把 web/dist 拷进 dist/public)
12
+ // FC_SERVER_PORT 是 FC 运行时注入的端口;PORT 给本地调试。
19
13
  async function bootstrap() {
20
14
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
21
15
 
22
16
  const isDev = process.env.NODE_ENV !== 'production';
23
- const isClawdDev = !!process.env.CLAWD_PREVIEW_PORT;
24
- const port =
25
- Number(process.env.CLAWD_PREVIEW_PORT) ||
26
- Number(process.env.FC_SERVER_PORT) ||
27
- Number(process.env.PORT) ||
28
- 3000;
17
+ const port = Number(process.env.FC_SERVER_PORT) || Number(process.env.PORT) || 3000;
29
18
 
30
- // 子路径 prefix:clawd dev 模式下挂 /preview/<port>/,其它(prod / 本地 vite 独跑)走根路径。
31
- const subPath = isClawdDev ? `preview/${port}` : '';
32
- const apiPrefix = subPath ? `${subPath}/api` : 'api';
33
- const viteBase = subPath ? `/${subPath}/` : '/';
34
-
35
- app.setGlobalPrefix(apiPrefix, { exclude: ['/'] });
19
+ app.setGlobalPrefix('api', { exclude: ['/'] });
36
20
 
37
21
  // 无登录门禁:页面打开即用。数据访问鉴权由后端用 DATACLAW_API_KEY(建页人提供的 apikey)注入,
38
22
  // 见 dataclaw.client —— key 绝不下发浏览器。
39
23
 
40
- if (isDev && isClawdDev) {
41
- // dev 模式 + 通过 clawd 启的 → mount vite middleware,root=../../web
42
- const { createServer: createViteServer } = await import('vite');
43
- const viteRoot = path.resolve(__dirname, '..', '..', 'web');
44
- // vite 6+ 默认 server.allowedHosts: [](只放 localhost / .localhost / IP)防 DNS rebinding。
45
- // frpc tunnel 转发过来的 Host header 是 daemon 当前 subdomain(CLAWD_TUNNEL_HOST 由
46
- // daemon supervisor 注入),不在白名单 → vite middleware 直接拒(老板 tunnel URL 404 根因)。
47
- // 显式把它加进 allowedHosts。
48
- const tunnelHost = process.env.CLAWD_TUNNEL_HOST || '';
49
- const allowedHosts = ['localhost', '127.0.0.1', ...(tunnelHost ? [tunnelHost] : [])];
50
- const vite = await createViteServer({
51
- root: viteRoot,
52
- // **显式 inline base**:不依赖 web/vite.config.js 解析路径(middleware mode + ESM 配置
53
- // 文件解析有时不可靠)。vite 据此识别带前缀的请求 + transformIndexHtml 注入带前缀的 asset URL。
54
- base: viteBase,
55
- server: {
56
- middlewareMode: true,
57
- // HMR WS 复用 nest 的 http.Server —— 跟 HTTP 共享同一个 fd / 同一个端口。
58
- // 不设这个 vite 会 fallback standalone 用 24678,多 project 并发会撞。
59
- hmr: { server: app.getHttpServer() },
60
- allowedHosts,
61
- },
62
- // appType: 'spa' 让 vite middleware 自己 serve index.html + fallback 路由(base 下任何
63
- // 非 vite asset 路径都返 index.html)。
64
- appType: 'spa',
65
- });
66
- // /api/* 守卫:实测 vite 'spa' catch-all 会先吃掉 /api/* 返 HTML,导致 API 拿不到 JSON。
67
- // Express middleware 栈顺序:vite.middlewares 这一行加进去时 nest controllers 还没挂
68
- // (nest router init 在 app.listen 时),所以 vite 拦截优先于 nest router → 命中 /api 也走 vite。
69
- // 守卫:req.url 命中 apiPrefix 时直接 next() 跳过 vite,让 nest router 接管返 JSON。
70
- const apiPathPrefix = `/${apiPrefix}/`; // 如 '/preview/6173/api/' 或 '/api/'
71
- app.use((req: any, res: any, next: any) => {
72
- if (typeof req.url === 'string' && req.url.startsWith(apiPathPrefix)) {
73
- return next(); // skip vite,让 nest router 处理 /api/*
74
- }
75
- return vite.middlewares(req, res, next);
76
- });
77
- console.log(`[clawd-dev] vite middleware mounted (root=${viteRoot}, base=${viteBase}, apiPrefix=${apiPrefix}, allowedHosts=${allowedHosts.join(',')})`);
78
- } else {
79
- // prod / 脱离 clawd 跑:直接 serve 前端静态资源。
80
- //
81
- // 路径解析两条路(覆盖 FC 部署与本地 monorepo 启动两种场景):
82
- // 1. FC(CODE_DIR=./server 只上传 server/,运行时 __dirname=dist/)
83
- // publish.sh BUILD_CMD 末尾会跑 `mkdir -p dist/public && cp -R ../web/dist/. dist/public/`
84
- // 把前端打包产物塞进 dist/public,跟着 server 一起上传到 FC。所以 prod 优先用
85
- // __dirname/public 这条 FC 内可达的绝对路径。
86
- // 2. 本地 monorepo `node dist/main.js`(极少用,但保留兼容):__dirname/public 不存在
87
- // 时 fallback 到 ../../web/dist(monorepo 相对路径)。
88
- //
89
- // 历史坑:spec 2026-06-03 前只有路径 2,FC 上 404 整个前端 —— 因为 ../../web/dist 在 FC
90
- // 容器里压根不存在(CODE_DIR 只上传了 server/)。dev 时靠 vite middleware 走内存编译没暴露。
91
- const fs = await import('node:fs');
92
- const fcStaticDir = path.resolve(__dirname, 'public');
93
- const monorepoStaticDir = path.resolve(__dirname, '..', '..', 'web', 'dist');
94
- const staticDir = fs.existsSync(fcStaticDir) ? fcStaticDir : monorepoStaticDir;
24
+ // 静态资源两条路(覆盖 FC 部署与本地 monorepo 启动两种场景):
25
+ // 1. FC(CODE_DIR=./server 只上传 server/,运行时 __dirname=dist/):publish.sh BUILD_CMD 末尾
26
+ // 把 ../web/dist 拷进 dist/public,跟着 server 一起上传,所以优先用 __dirname/public
27
+ // 2. 本地 `node dist/main.js`:__dirname/public 不存在时 fallback 到 ../../web/dist
28
+ const fs = await import('node:fs');
29
+ const fcStaticDir = path.resolve(__dirname, 'public');
30
+ const monorepoStaticDir = path.resolve(__dirname, '..', '..', 'web', 'dist');
31
+ const staticDir = fs.existsSync(fcStaticDir) ? fcStaticDir : monorepoStaticDir;
32
+ if (fs.existsSync(staticDir)) {
95
33
  app.useStaticAssets(staticDir);
96
- console.log(`[prod] serving static from ${staticDir}`);
34
+ console.log(`serving static from ${staticDir}`);
97
35
  }
98
36
 
99
37
  await app.listen(port, '0.0.0.0');
100
- console.log(`server listening on :${port} (${isDev ? 'dev' : 'prod'}${isClawdDev ? ' / clawd' : ''})`);
38
+ console.log(`server listening on :${port} (${isDev ? 'dev' : 'prod'})`);
101
39
  }
102
40
 
103
41
  bootstrap();
@@ -1,14 +1,9 @@
1
1
  import { defineConfig } from 'vite';
2
2
  import react from '@vitejs/plugin-react';
3
3
 
4
- // app-builder G 方案:dev 时 vite 不独立跑,而是被 server/src/main.ts 的
5
- // `createViteServer({ middlewareMode: true })` 当 middleware 挂进 nest,前端 HMR
6
- // 和 /api 都从 nest 监听的 CLAWD_PREVIEW_PORT 出(spec 2026-06-01 §5.6)。
7
- // 这个 config 仅用于:
8
- // 1. `vite build` 生成 web/dist 静态资源(prod 由 nest serve)
9
- // 2. 脱离 clawd 直接 `pnpm dev` 跑 vite 独立模式(本地调试)
10
- const tunnelHost = process.env.CLAWD_TUNNEL_HOST ?? '';
11
- const port = Number(process.env.CLAWD_PREVIEW_PORT ?? 5173);
4
+ // dev:`pnpm dev` 独跑 vite,/api 代理到本地 nest(server 默认 3000,API_PORT 可改);
5
+ // build:产物进 web/dist,prod 由 nest serve(publish.sh 把它拷进 server/dist/public)。
6
+ const apiTarget = `http://127.0.0.1:${process.env.API_PORT ?? 3000}`;
12
7
 
13
8
  export default defineConfig({
14
9
  plugins: [react()],
@@ -16,15 +11,9 @@ export default defineConfig({
16
11
  outDir: 'dist',
17
12
  emptyOutDir: true,
18
13
  },
19
- // 隧道经 nest 主进程进来,vite middleware 自己识别 base。standalone 模式(无 tunnel)
20
- // 走 base: '/' 方便本地直接打开。
21
- base: tunnelHost ? `/preview/${port}/` : '/',
22
14
  server: {
23
15
  host: '127.0.0.1',
24
- port,
25
- strictPort: true,
26
- hmr: tunnelHost
27
- ? { protocol: 'wss', clientPort: 443, path: `/preview/${port}/`, host: tunnelHost }
28
- : undefined,
16
+ port: Number(process.env.PORT ?? 5173),
17
+ proxy: { '/api': apiTarget },
29
18
  },
30
19
  });
@@ -21224,7 +21224,7 @@ async function main() {
21224
21224
  "call",
21225
21225
  {
21226
21226
  title: "Call a clawd daemon RPC method",
21227
- description: 'Call a clawd daemon RPC method over the authenticated local channel. `method` is the canonical method name (e.g. "attachment.signUrl", "persona:list", "appBuilder:createProject"); `args` is the JSON body for that method. sessionId is auto-filled from this session unless explicitly provided in args. The daemon is the sole authority on method existence and authorization \u2014 unknown methods or missing permissions surface as errors from the daemon.',
21227
+ description: 'Call a clawd daemon RPC method over the authenticated local channel. `method` is the canonical method name (e.g. "attachment.signUrl", "persona:list", "session:list"); `args` is the JSON body for that method. sessionId is auto-filled from this session unless explicitly provided in args. The daemon is the sole authority on method existence and authorization \u2014 unknown methods or missing permissions surface as errors from the daemon.',
21228
21228
  inputSchema: {
21229
21229
  method: external_exports.string().min(1).describe('canonical RPC method name, e.g. "persona:list"'),
21230
21230
  args: external_exports.record(external_exports.unknown()).optional().describe("JSON arguments for the method")