@clawos-dev/clawd 0.2.142 → 0.2.143-beta.300.c2a3792
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.
- package/dist/cli.cjs +664 -1367
- package/dist/persona-defaults/persona-app-builder/CLAUDE.md +0 -3
- package/dist/persona-defaults/persona-bug-fixer/CLAUDE.md +0 -2
- package/dist/persona-defaults/persona-dataclaw-builder/.mcp.json +4 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.secrets/dataclaw.env.example +3 -0
- package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +20 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/README.md +96 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/config.env +20 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/contract/bootstrap +22 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/contract/s.yaml.tmpl +59 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/.env.example +15 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/.fcignore +7 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/nest-cli.json +8 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/package.json +29 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +13 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/auth/gate.ts +70 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/auth/session.ts +52 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/auth/ttc-user-info.ts +23 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.client.ts +83 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.config.ts +18 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.controller.ts +30 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.module.ts +9 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/main.ts +116 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +6 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/tsconfig.json +14 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/index.html +12 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/package.json +18 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +9 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/api.js +19 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/auth.js +7 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/BarChart.jsx +38 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/FilterRow.jsx +79 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultCards.jsx +17 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultTable.jsx +21 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/SchemaFilters.jsx +93 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/enums.js +22 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/main.jsx +6 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/pages/PerformanceQueryPage.jsx +102 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/styles.css +83 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/vite.config.js +30 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/scripts/ensure-toolchain.sh +62 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/scripts/new-extension.sh +66 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/scripts/publish.sh +129 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/scripts/remove-extension.sh +70 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/scripts/verify.sh +38 -0
- package/dist/persona-defaults/persona-developer/CLAUDE.md +0 -2
- package/package.json +2 -5
- package/dist/dispatch/mcp-server.cjs +0 -39756
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
- **你的文件工作目录**:如果连接上下文(connection prompt)里写了"你的文件工作目录是 xxx",那就是当前用户的专属目录,所有文件读写都放在那里;项目目录在它下面的 `projects/<name>/`
|
|
11
11
|
- **persona 目录只读**:persona 自己的目录(人格 / extension-kit 资产)对你只读,不要往里写任何文件
|
|
12
12
|
- **路径永远以 createProject 返回的 `projectDir` 为准**:不要假设、不要拼接、不要写死任何项目路径
|
|
13
|
-
- **⚠️ dispatch 陷阱(被另一个 persona 委派来时常踩)**:任务包里 dispatcher(A persona)写的"建议路径"如 `~/dev/xxx` / `/tmp/xxx` 之类**全部不能信**——dispatcher 不懂 app-builder 的目录规范,照搬就会把项目落在错误位置(用户工作区之外,UI 看不到、daemon 不管理、下次切 session 全丢)。**不管任务包怎么写,路径都必须走 `appBuilder.createProject` RPC**让 daemon 派生正确的用户工作目录;你的 cwd(spawn 时 daemon 设的)就是当前用户的专属工作目录,所有项目必须落在它下面的 `projects/<name>/`。如果连 cwd 都还没确定(dispatch session 没 connection prompt),先调 `appBuilder.listProjects` 看一眼现状再开工。
|
|
14
13
|
- **extension-kit 引用用绝对路径**:你的 cwd 在用户工作目录、**不在 persona 目录**,所以本文档里所有 `extension-kit/` 脚本(new-extension.sh / publish.sh)执行时要用绝对路径 `$HOME/.clawd/personas/persona-app-builder/extension-kit/`(daemon 维护的 persona 资产目录)
|
|
15
14
|
|
|
16
15
|
## 何时找你
|
|
@@ -308,5 +307,3 @@ await app.listen(Number(process.env.CLAWD_PREVIEW_PORT));
|
|
|
308
307
|
- 部署前后都验证服务真的起来了(健康检查 / 公网访问 / 看日志),不靠"应该没问题"
|
|
309
308
|
- 增量提交可工作的代码 / 从既有实现学 / 3 次失败停下重新评估
|
|
310
309
|
- 错误处理快速失败 + 描述清楚 + 不静默吞异常
|
|
311
|
-
|
|
312
|
-
> dispatch 教学(`@persona/<id>` 委派 / `personaDispatchComplete` 回报)由 daemon 通过 `--append-system-prompt` 给所有 cc spawn 统一注入,不在这份 CLAUDE.md 重复。源见 `daemon/src/dispatch/system-prompt.ts`。
|
|
@@ -98,5 +98,3 @@ done
|
|
|
98
98
|
⚠️ **`.mcp.json` 是 daemon 管理文件**(在 `DAEMON_MANAGED_PATHS` 里),每次 daemon 启动会用 bundle 版本覆盖,**不要手改本地**——要改 MCP 配置去改 daemon defaults 源 + 提 PR。
|
|
99
99
|
|
|
100
100
|
**要加别的 MCP server**(项目专属 / 个人用):在**项目目录**(不是 persona 目录)下加 `.mcp.json`,跟 persona 这份不冲突。或者扩 daemon defaults 给所有用户。
|
|
101
|
-
|
|
102
|
-
> dispatch 教学(`@persona/<id>` 委派 / `personaDispatchComplete` 回报)由 daemon 通过 `--append-system-prompt` 给所有 cc spawn 统一注入,不在这份 CLAUDE.md 重复。源见 `daemon/src/dispatch/system-prompt.ts`。
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# persona-dataclaw-builder
|
|
2
|
+
|
|
3
|
+
你是 clawd 里的「数据查询搭建师」。一句话定位:**帮 TTC 业务用户把特定的数据查询条件,做成一个只读的 GUI 查询页面,背后复用 dataclaw 的查询能力。** 你做出来的页面是独立 app,用 app-builder 流水线起、双栏实时预览、发阿里云 FC。
|
|
4
|
+
|
|
5
|
+
## 工作流(复用 app-builder 流水线)
|
|
6
|
+
与 persona-app-builder 同一套 daemon HTTP RPC(`$CLAWD_DAEMON_URL/api/rpc/appBuilder.*` + `X-Clawd-Session-Id`):fresh session 起手问「想看什么数据的页?」→ 反问项目名 → `appBuilder.createProject` → `reportStage('installing')` + `pnpm install` → `appBuilder.startDevServer`(双栏预览)→ publish。RPC 细节、stage 规则、失败上报同 persona-app-builder/CLAUDE.md。
|
|
7
|
+
|
|
8
|
+
## 数据来源:dataclaw-service(只读,通路 A)
|
|
9
|
+
生成的 app 后端直调 dataclaw-service HTTP(`/api/flexible-query`、`/api/intent-execute`、`/api/schema`),用**建页人(builder)的身份**取数、纯展示、不写数据。具体查询能力见 §「dataclaw 查询能力」。
|
|
10
|
+
|
|
11
|
+
## dataclaw 查询能力
|
|
12
|
+
本章节在后续模板/知识充实阶段填全(来源:dataclaw-src 仓库 workspace/docs/dataclaw-builder-architecture.md)。当前仅建骨架。
|
|
13
|
+
|
|
14
|
+
## 双客户
|
|
15
|
+
业务用户自助建简单页(语气业务化、屏蔽技术细节);管理员代建复杂页(可技术化)。
|
|
16
|
+
|
|
17
|
+
## 红线
|
|
18
|
+
- 只读:绝不让生成的 app 往 dataclaw 写数据 / 不接 Supabase 写库。
|
|
19
|
+
- 数据范围是隐式 AND(按建页人身份兜),**绝不把 scope(self/team/all)做成用户可选下拉**——等于越权。
|
|
20
|
+
- persona 目录只读;项目路径以 createProject 返回的 projectDir 为准。
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# extension-kit —— 从零到发布的 extension 流水线
|
|
2
|
+
|
|
3
|
+
把「前端 + 后端 + Supabase 的 extension 发布到阿里云 FC(serverless,免 Docker)」固化成可复用流水线。开发靠人/agent,发布全自动。
|
|
4
|
+
|
|
5
|
+
## 设计理念:契约层 + 可插拔技术栈
|
|
6
|
+
|
|
7
|
+
FC 对应用只有一条要求:**一个监听 `$PORT` 的 HTTP server**。所以固化的是「发布管道」,不是技术栈。
|
|
8
|
+
|
|
9
|
+
- **契约层(框架/语言无关,固定)**:`contract/` + `scripts/` + `config.env`
|
|
10
|
+
- **技术栈示例(可换的起点)**:`examples/`(默认 `nestjs-react`)
|
|
11
|
+
|
|
12
|
+
## 目录
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
extension-kit/
|
|
16
|
+
├── config.env 平台级固定变量(region / Node层 / Supabase)
|
|
17
|
+
├── contract/
|
|
18
|
+
│ ├── bootstrap FC 启动入口(Node 参考实现:find node + cd + exec)
|
|
19
|
+
│ └── s.yaml.tmpl FC 部署模板(占位符)
|
|
20
|
+
├── scripts/
|
|
21
|
+
│ ├── new-extension.sh 从示例生成新 extension + 写 ext.conf
|
|
22
|
+
│ ├── publish.sh 一键发布(build → 部署 → 绑域名 → 验证)
|
|
23
|
+
│ ├── verify.sh 健康检查(含「是否被强制下载」检测)
|
|
24
|
+
│ └── remove-extension.sh 清理(s remove FC 资源 + 按 ext.conf 删 Supabase 表)
|
|
25
|
+
└── examples/nestjs-react/ 默认示例(就是跑通的留言板)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 用法
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 1) 起一个新 extension(从默认示例)
|
|
32
|
+
extension-kit/scripts/new-extension.sh myapp
|
|
33
|
+
|
|
34
|
+
# 2) 设计 Supabase 表(务必 myapp_ 前缀)、改 web/ 和 server/ 业务逻辑
|
|
35
|
+
# 建表后把表名填进 myapp/ext.conf 的 SUPABASE_TABLES,清理时才删得掉
|
|
36
|
+
# 本地跑: cd myapp/server && npm i && npm run build && node --env-file=.env dist/main.js
|
|
37
|
+
|
|
38
|
+
# 3) 一键发布到 FC,输出公网地址
|
|
39
|
+
extension-kit/scripts/publish.sh ../myapp
|
|
40
|
+
|
|
41
|
+
# 4) 不要了就一键清理(FC 资源 + 它的 Supabase 表)
|
|
42
|
+
extension-kit/scripts/remove-extension.sh ../myapp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 流程与固化程度
|
|
46
|
+
|
|
47
|
+
| 步骤 | 谁做 |
|
|
48
|
+
|------|------|
|
|
49
|
+
| new-extension 脚手架 | 🟢 脚本 |
|
|
50
|
+
| 设计 Supabase schema | 🔴 人/agent(每个 extension 不同) |
|
|
51
|
+
| 写前后端业务 | 🔴 人/agent |
|
|
52
|
+
| publish(build/部署/域名/验证) | 🟢 脚本 |
|
|
53
|
+
| remove(清 FC + 表) | 🟢 脚本 |
|
|
54
|
+
|
|
55
|
+
## 清理一个 extension
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
extension-kit/scripts/remove-extension.sh ../myapp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
先 `s remove` 删 FC 函数+触发器+自定义域名,再按 `ext.conf` 的 `SUPABASE_TABLES` 删它的表。
|
|
62
|
+
|
|
63
|
+
**脚本怎么知道删哪个表**:靠 `ext.conf` 里显式声明的 `SUPABASE_TABLES="myapp_xxx"`(建表时填进去,extension 的"自描述")。三道安全锁:
|
|
64
|
+
|
|
65
|
+
1. 每个表必须 `<app>_` 前缀,否则**拒绝删**(物理上防误伤 clawos 共享库)
|
|
66
|
+
2. 删前列出所有要删的表
|
|
67
|
+
3. 要手输 `yes` 确认(数据不可逆,不用 `-y` 静默删)
|
|
68
|
+
|
|
69
|
+
> SLS 日志 project / OSS 代码桶是**账号级共享**(多 extension 共用),不随单个 extension 删。
|
|
70
|
+
|
|
71
|
+
## 换框架(同 Node,几乎零成本)
|
|
72
|
+
|
|
73
|
+
改 extension 目录里的 `ext.conf`:
|
|
74
|
+
- `FC_ENTRY` —— 入口文件(Express: `index.js`;NestJS: `dist/main.js`;Fastify/Hono 同理)
|
|
75
|
+
- `BUILD_CMD` —— 构建命令
|
|
76
|
+
|
|
77
|
+
`contract/bootstrap` 和 `s.yaml.tmpl` **不用动**。只要你的 server 最终 `listen(process.env.PORT)` 即可。
|
|
78
|
+
|
|
79
|
+
## 换语言(Node → Python/Go…)
|
|
80
|
+
|
|
81
|
+
1. `config.env` 的 `NODE_LAYER` 换成对应语言的 FC 运行时层
|
|
82
|
+
2. 替换 `contract/bootstrap`(如 `exec python app.py`)
|
|
83
|
+
3. `ext.conf` 的 `BUILD_CMD` 改成对应构建命令
|
|
84
|
+
|
|
85
|
+
`s.yaml.tmpl` 结构不变。
|
|
86
|
+
|
|
87
|
+
## 红线(都是踩坑换来的,见记忆 fc-nodejs-deploy-recipe)
|
|
88
|
+
|
|
89
|
+
- **Supabase 是 clawos 共享生产库**:新表必须 `<app>_` 前缀,**绝不 drop/alter clawos 已有表**
|
|
90
|
+
- **必须绑自定义域名**(fc3-domain auto),否则 `fcapp.run` 默认 URL 强制下载网页、浏览器不渲染
|
|
91
|
+
- **不用 SAE**(Node 无代码包,只能镜像=要 Docker);不用 `custom.debian10`/裸 command(node 找不到 / CAExited)
|
|
92
|
+
- **长连接 / WebSocket 推送不适合 FC** → 用 Supabase Realtime(FC 不维持长连接)
|
|
93
|
+
|
|
94
|
+
## 不固化的(留给人/agent)
|
|
95
|
+
|
|
96
|
+
数据模型设计、业务逻辑、建表前的归属判断、发布失败时的异常排查。
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# ===== 平台级固定变量(一次配好,所有 extension 共用)=====
|
|
2
|
+
# 阿里云凭证不在这里:从 .secrets/aliyun.env 现读(见 CLAUDE.md)
|
|
3
|
+
|
|
4
|
+
REGION=cn-hangzhou
|
|
5
|
+
|
|
6
|
+
# FC 运行时:custom.debian12(纯净) + 官方 Node22 层(自带 node + 原生 WebSocket)
|
|
7
|
+
# 换语言时改这两行 + 换 contract/bootstrap(见 README「换框架/语言」)
|
|
8
|
+
FC_RUNTIME=custom.debian12
|
|
9
|
+
NODE_LAYER=acs:fc:cn-hangzhou:official:layers/Nodejs22/versions/1
|
|
10
|
+
|
|
11
|
+
# dataclaw-service(通路 A,只读)—— 部署后填实际可达地址
|
|
12
|
+
DATACLAW_SERVICE_URL=
|
|
13
|
+
# TTC 飞书登录门禁(无密钥经纪,只重定向到 TTC)
|
|
14
|
+
TTC_AUTH_BASE=https://app.ttcadvisory.com
|
|
15
|
+
TTC_API_BASE=https://api.ttcadvisory.com
|
|
16
|
+
|
|
17
|
+
# FC 资源规格(够跑轻量 extension;按需改)
|
|
18
|
+
FC_CPU=0.35
|
|
19
|
+
FC_MEMORY=512
|
|
20
|
+
FC_TIMEOUT=60
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# FC custom runtime 启动入口 —— 框架无关的 Node 参考实现
|
|
4
|
+
# ============================================================
|
|
5
|
+
# 契约: FC 只要求"一个监听 $PORT 的 HTTP server"。本脚本负责把它拉起来。
|
|
6
|
+
# - 换 Node 框架(Express/Fastify/NestJS/Hono...): 只改 s.yaml 的 APP_ENTRY 环境变量
|
|
7
|
+
# 指向你的入口文件(如 index.js / dist/main.js),本文件不用动。
|
|
8
|
+
# - 换语言(Python/Go...): 替换本文件(如 `exec python app.py`),并在 config.env
|
|
9
|
+
# 换 NODE_LAYER 为对应语言的运行时层。
|
|
10
|
+
# ------------------------------------------------------------
|
|
11
|
+
cd "$(dirname "$0")" || exit 1
|
|
12
|
+
|
|
13
|
+
# custom.debian* 不自带 node,从挂载的官方 Node 层(/opt)动态定位,不赌固定路径
|
|
14
|
+
NODE_BIN="$(command -v node || find /opt -maxdepth 6 -type f -name node 2>/dev/null | head -1)"
|
|
15
|
+
if [ -z "$NODE_BIN" ]; then
|
|
16
|
+
echo "[bootstrap] FATAL: node 不在 PATH 也不在 /opt,检查是否挂了 Node 运行时层" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
ENTRY="${APP_ENTRY:-dist/main.js}"
|
|
21
|
+
echo "[bootstrap] node=$NODE_BIN entry=$ENTRY cwd=$(pwd) port=${PORT:-9000}"
|
|
22
|
+
exec "$NODE_BIN" "$ENTRY"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# s.yaml 模板 —— publish.sh 用 sed 替换 __占位符__ 后生成实际 s.yaml
|
|
2
|
+
# 框架无关:command 固定走 ./bootstrap,入口由 FC_ENTRY 环境变量决定
|
|
3
|
+
edition: 3.0.0
|
|
4
|
+
name: __APP_NAME__-app
|
|
5
|
+
access: default
|
|
6
|
+
|
|
7
|
+
vars:
|
|
8
|
+
region: __REGION__
|
|
9
|
+
|
|
10
|
+
resources:
|
|
11
|
+
__APP_NAME__:
|
|
12
|
+
component: fc3
|
|
13
|
+
props:
|
|
14
|
+
region: ${vars.region}
|
|
15
|
+
functionName: __APP_NAME__
|
|
16
|
+
description: __APP_NAME__ extension (FC serverless)
|
|
17
|
+
runtime: __FC_RUNTIME__
|
|
18
|
+
logConfig: auto
|
|
19
|
+
layers:
|
|
20
|
+
- __NODE_LAYER__
|
|
21
|
+
cpu: __FC_CPU__
|
|
22
|
+
memorySize: __FC_MEMORY__
|
|
23
|
+
diskSize: 512
|
|
24
|
+
timeout: __FC_TIMEOUT__
|
|
25
|
+
instanceConcurrency: 10
|
|
26
|
+
code: __CODE_DIR__
|
|
27
|
+
customRuntimeConfig:
|
|
28
|
+
command:
|
|
29
|
+
- ./bootstrap
|
|
30
|
+
port: 9000
|
|
31
|
+
environmentVariables:
|
|
32
|
+
PORT: '9000'
|
|
33
|
+
APP_ENTRY: __FC_ENTRY__
|
|
34
|
+
DATACLAW_SERVICE_URL: __DATACLAW_SERVICE_URL__
|
|
35
|
+
DATACLAW_AS_UNION_ID: __DATACLAW_AS_UNION_ID__
|
|
36
|
+
DATACLAW_INTERNAL_SECRET: __DATACLAW_INTERNAL_SECRET__
|
|
37
|
+
TTC_AUTH_BASE: __TTC_AUTH_BASE__
|
|
38
|
+
TTC_API_BASE: __TTC_API_BASE__
|
|
39
|
+
APP_BASE_URL: __APP_BASE_URL__
|
|
40
|
+
SESSION_SECRET: __SESSION_SECRET__
|
|
41
|
+
triggers:
|
|
42
|
+
- triggerName: httpTrigger
|
|
43
|
+
triggerType: http
|
|
44
|
+
triggerConfig:
|
|
45
|
+
authType: anonymous
|
|
46
|
+
methods: [GET, POST, PUT, DELETE, HEAD]
|
|
47
|
+
|
|
48
|
+
# 自定义域名:否则 fcapp.run 默认 URL 强制下载网页(防滥用),浏览器无法渲染
|
|
49
|
+
__APP_NAME__Domain:
|
|
50
|
+
component: fc3-domain
|
|
51
|
+
props:
|
|
52
|
+
region: ${vars.region}
|
|
53
|
+
domainName: auto
|
|
54
|
+
protocol: HTTP
|
|
55
|
+
routeConfig:
|
|
56
|
+
routes:
|
|
57
|
+
- path: /*
|
|
58
|
+
functionName: __APP_NAME__
|
|
59
|
+
qualifier: LATEST
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# dataclaw 通路 A(后端直调 dataclaw-service HTTP,只读)
|
|
2
|
+
DATACLAW_SERVICE_URL=http://dataclaw-service:8080
|
|
3
|
+
# 建页人 builder 的 union_id —— 数据范围按此身份在 dataclaw 侧自动裁剪
|
|
4
|
+
DATACLAW_AS_UNION_ID=on_xxxxxxxxxxxxxxxx
|
|
5
|
+
# 服务间共享密钥(与 dataclaw-service 的 internal secret 对齐);勿提交真实值
|
|
6
|
+
DATACLAW_INTERNAL_SECRET=replace-me
|
|
7
|
+
# 本地起页跳过门禁(仅 dev;上线删或置 0)
|
|
8
|
+
DATACLAW_AUTH_DISABLED=1
|
|
9
|
+
PORT=3000
|
|
10
|
+
|
|
11
|
+
# TTC 飞书登录门禁(无密钥经纪:重定向到 TTC,不持飞书密钥)
|
|
12
|
+
TTC_AUTH_BASE=https://app.ttcadvisory.com
|
|
13
|
+
TTC_API_BASE=https://api.ttcadvisory.com
|
|
14
|
+
APP_BASE_URL=https://your-app.example.com # 发布后的公网地址,回调用;HTTPS(cookie secure)
|
|
15
|
+
SESSION_SECRET=replace-with-openssl-rand-hex-32
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "guestbook-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "nest build",
|
|
7
|
+
"start": "node --env-file=.env dist/main.js",
|
|
8
|
+
"start:dev": "nest start --watch",
|
|
9
|
+
"dev": "nest start --watch"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@nestjs/common": "^10.4.0",
|
|
13
|
+
"@nestjs/core": "^10.4.0",
|
|
14
|
+
"@nestjs/platform-express": "^10.4.0",
|
|
15
|
+
"cookie-parser": "^1.4.6",
|
|
16
|
+
"dotenv": "^16.4.5",
|
|
17
|
+
"reflect-metadata": "^0.2.2",
|
|
18
|
+
"rxjs": "^7.8.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@nestjs/cli": "^10.4.0",
|
|
22
|
+
"@nestjs/schematics": "^10.2.0",
|
|
23
|
+
"@types/cookie-parser": "^1.4.7",
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"@vitejs/plugin-react": "^4.4.0",
|
|
26
|
+
"typescript": "^5.5.0",
|
|
27
|
+
"vite": "^6.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { DataclawModule } from './dataclaw/dataclaw.module';
|
|
3
|
+
|
|
4
|
+
// dataclaw 只读后端 BFF 起手骨架:默认只注册 DataclawModule(schema / query / intent /
|
|
5
|
+
// consultants 四个只读端点,直调 dataclaw-service,身份按建页人 union_id 裁剪)。
|
|
6
|
+
// 业务逻辑由 assistant 按用户需求从零加(添 controller/module 进 imports 即可)。
|
|
7
|
+
//
|
|
8
|
+
// vite middleware / 静态资源 serve 路径仍由 main.ts 处理(dev mount vite,prod express.static),
|
|
9
|
+
// 不需要 ServeStaticModule。
|
|
10
|
+
@Module({
|
|
11
|
+
imports: [DataclawModule],
|
|
12
|
+
})
|
|
13
|
+
export class AppModule {}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// TTC 飞书登录门禁(无密钥经纪):Express 中间件 + /auth/login、/auth/callback 处理器。
|
|
2
|
+
// 必须挂在 vite middleware(dev) / express.static(prod) 之前——前端 HTML 不走 Nest 路由,
|
|
3
|
+
// Nest guard 拦不住,所以门禁只能是 Express 层中间件(main.ts 接线在 vite/static 之前)。
|
|
4
|
+
// 门禁只决定"能不能打开页";数据范围仍由 dataclaw client 固定用建页人 union_id(PR-4 已做),
|
|
5
|
+
// 不用登录者身份查数。
|
|
6
|
+
import { fetchTtcUser } from './ttc-user-info'
|
|
7
|
+
import { makeSession, readSession, makeState, verifyState } from './session'
|
|
8
|
+
|
|
9
|
+
// req/res/next 用 any:express 不是本 server 包的直接依赖(pnpm 下从 server 根 require.resolve
|
|
10
|
+
// 'express' 失败,main.ts:67 同样用 any),从 'express' import 类型会编译失败。
|
|
11
|
+
type Req = any
|
|
12
|
+
type Res = any
|
|
13
|
+
type Next = (err?: unknown) => void
|
|
14
|
+
|
|
15
|
+
const SESSION_COOKIE = 'dc_session'
|
|
16
|
+
const STATE_COOKIE = 'dc_oauth_state'
|
|
17
|
+
|
|
18
|
+
// 子路径部署(clawd dev preview/<port>/,见 main.ts subPath 逻辑)下 redirect 要带同样前缀;
|
|
19
|
+
// prod base=/ 时无前缀,返回 '/auth/login'。
|
|
20
|
+
function loginPath(req: Req): string {
|
|
21
|
+
const url = String(req.originalUrl || req.url || '')
|
|
22
|
+
const m = url.match(/^\/(preview\/\d+)\//)
|
|
23
|
+
return m ? `/${m[1]}/auth/login` : '/auth/login'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 门禁中间件:放行 /auth/*;DATACLAW_AUTH_DISABLED=1 全放行;否则无有效 session → 302 /auth/login。
|
|
27
|
+
export function authGate(req: Req, res: Res, next: Next) {
|
|
28
|
+
if (process.env.DATACLAW_AUTH_DISABLED === '1') return next()
|
|
29
|
+
const p = String(req.path || req.url || '')
|
|
30
|
+
if (p.includes('/auth/login') || p.includes('/auth/callback')) return next()
|
|
31
|
+
const sess = readSession(req.cookies?.[SESSION_COOKIE])
|
|
32
|
+
if (sess) return next()
|
|
33
|
+
return res.redirect(302, loginPath(req))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loginHandler(req: Req, res: Res) {
|
|
37
|
+
const authBase = process.env.TTC_AUTH_BASE || 'https://app.ttcadvisory.com'
|
|
38
|
+
const appBase = (process.env.APP_BASE_URL || '').replace(/\/+$/, '')
|
|
39
|
+
const state = makeState()
|
|
40
|
+
res.cookie(STATE_COOKIE, state, { httpOnly: true, sameSite: 'lax', secure: true, maxAge: 5 * 60 * 1000 })
|
|
41
|
+
const url = new URL('/auth/authorize', authBase)
|
|
42
|
+
url.searchParams.set('callback_url', `${appBase}/auth/callback`)
|
|
43
|
+
url.searchParams.set('state', state)
|
|
44
|
+
url.searchParams.set('auto', '1')
|
|
45
|
+
res.redirect(302, url.toString())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function callbackHandler(req: Req, res: Res) {
|
|
49
|
+
const token = String(req.query?.token || '')
|
|
50
|
+
const state = String(req.query?.state || '')
|
|
51
|
+
const cookieState = req.cookies?.[STATE_COOKIE]
|
|
52
|
+
// CSRF:回调 state 必须等于 cookie 里的、且签名有效未过期
|
|
53
|
+
if (!token || !state || state !== cookieState || !verifyState(state)) {
|
|
54
|
+
return res.status(401).send('登录校验失败,请重试')
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const apiBase = process.env.TTC_API_BASE || 'https://api.ttcadvisory.com'
|
|
58
|
+
const user = await fetchTtcUser(apiBase, token) // 只验身份;TTC token 验完即丢,不存不用于查数
|
|
59
|
+
res.clearCookie(STATE_COOKIE)
|
|
60
|
+
res.cookie(SESSION_COOKIE, makeSession(user.uniqueId, user.name), {
|
|
61
|
+
httpOnly: true,
|
|
62
|
+
sameSite: 'lax',
|
|
63
|
+
secure: true,
|
|
64
|
+
maxAge: 12 * 3600 * 1000,
|
|
65
|
+
})
|
|
66
|
+
return res.redirect(302, '/')
|
|
67
|
+
} catch {
|
|
68
|
+
return res.status(401).send('飞书身份校验失败')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// HMAC 签名 session + oauth state cookie,用 SESSION_SECRET 签发/校验。
|
|
2
|
+
// FC 多实例无共享内存,所以 state 也走签名 cookie,不能用进程内 Map。
|
|
3
|
+
import { createHmac, timingSafeEqual, randomBytes } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
const SECRET = () => process.env.SESSION_SECRET || ''
|
|
6
|
+
|
|
7
|
+
function sign(payload: string): string {
|
|
8
|
+
const mac = createHmac('sha256', SECRET()).update(payload).digest('base64url')
|
|
9
|
+
return `${Buffer.from(payload).toString('base64url')}.${mac}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function verify(token: string): string | null {
|
|
13
|
+
const [b64, mac] = (token || '').split('.')
|
|
14
|
+
if (!b64 || !mac) return null
|
|
15
|
+
const payload = Buffer.from(b64, 'base64url').toString()
|
|
16
|
+
const expect = createHmac('sha256', SECRET()).update(payload).digest('base64url')
|
|
17
|
+
const a = Buffer.from(mac)
|
|
18
|
+
const b = Buffer.from(expect)
|
|
19
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) return null
|
|
20
|
+
return payload
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// session:存 {uid,name,exp}(仅展示/审计,不用于查数——数据范围恒用建页人 union_id)。
|
|
24
|
+
export function makeSession(uid: string, name: string, ttlMs = 12 * 3600 * 1000): string {
|
|
25
|
+
return sign(JSON.stringify({ uid, name, exp: Date.now() + ttlMs }))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readSession(token?: string): { uid: string; name: string } | null {
|
|
29
|
+
const p = token && verify(token)
|
|
30
|
+
if (!p) return null
|
|
31
|
+
try {
|
|
32
|
+
const o = JSON.parse(p)
|
|
33
|
+
if (typeof o.exp === 'number' && o.exp > Date.now()) return { uid: o.uid, name: o.name }
|
|
34
|
+
} catch {}
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function makeState(): string {
|
|
39
|
+
const nonce = randomBytes(16).toString('base64url')
|
|
40
|
+
return sign(JSON.stringify({ nonce, exp: Date.now() + 5 * 60 * 1000 }))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function verifyState(token?: string): boolean {
|
|
44
|
+
const p = token && verify(token)
|
|
45
|
+
if (!p) return false
|
|
46
|
+
try {
|
|
47
|
+
const o = JSON.parse(p)
|
|
48
|
+
return typeof o.exp === 'number' && o.exp > Date.now()
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// 用 TTC JWT 调 user_info 验身份(无密钥经纪:app 不持飞书/TTC 密钥,只用回调 token 验身份)。
|
|
2
|
+
// 精简自 daemon feishu-auth/ttc-client.ts,独立实现、不 import daemon 代码。
|
|
3
|
+
// 接口响应结构:GET /api/user_service/v1/login/user → { code:0, data:{ unique_id, name, union_id, avatar_url } }
|
|
4
|
+
export interface TtcUser {
|
|
5
|
+
uniqueId: string
|
|
6
|
+
name: string
|
|
7
|
+
unionId?: string
|
|
8
|
+
avatarUrl?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function fetchTtcUser(apiBase: string, token: string, fetchImpl: typeof fetch = fetch): Promise<TtcUser> {
|
|
12
|
+
const res = await fetchImpl(`${apiBase}/api/user_service/v1/login/user`, { headers: { Authorization: `Bearer ${token}` } })
|
|
13
|
+
if (!res.ok) throw new Error(`TTC user_info HTTP ${res.status}`)
|
|
14
|
+
const body = (await res.json()) as { code?: number; data?: { unique_id?: string; name?: string; union_id?: string; avatar_url?: string } }
|
|
15
|
+
if (body.code !== 0 || !body.data?.unique_id || !body.data?.name) throw new Error('TTC user_info invalid')
|
|
16
|
+
const d = body.data
|
|
17
|
+
return {
|
|
18
|
+
uniqueId: d.unique_id,
|
|
19
|
+
name: d.name,
|
|
20
|
+
...(d.union_id ? { unionId: d.union_id } : {}),
|
|
21
|
+
...(d.avatar_url ? { avatarUrl: d.avatar_url } : {}),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Injectable, HttpException, OnModuleInit } from '@nestjs/common'
|
|
2
|
+
import { DataclawConfig, loadDataclawConfig } from './dataclaw.config'
|
|
3
|
+
|
|
4
|
+
// 端点超时(架构文档 §8.10):schema / flexible-query 走默认;intent-execute 可能跑 LLM 编排,给 90s。
|
|
5
|
+
const TIMEOUT_MS = { default: 30_000, intent: 90_000 }
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class DataclawClient implements OnModuleInit {
|
|
9
|
+
private cfg!: DataclawConfig
|
|
10
|
+
|
|
11
|
+
onModuleInit() {
|
|
12
|
+
this.cfg = loadDataclawConfig()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private async call<T>(
|
|
16
|
+
method: 'GET' | 'POST',
|
|
17
|
+
path: string,
|
|
18
|
+
opts: {
|
|
19
|
+
query?: Record<string, string | number | undefined>
|
|
20
|
+
body?: unknown
|
|
21
|
+
timeout?: number
|
|
22
|
+
} = {},
|
|
23
|
+
): Promise<T> {
|
|
24
|
+
const url = new URL(this.cfg.baseUrl + path)
|
|
25
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
26
|
+
if (v !== undefined && v !== '') url.searchParams.set(k, String(v))
|
|
27
|
+
}
|
|
28
|
+
const ctrl = new AbortController()
|
|
29
|
+
const t = setTimeout(() => ctrl.abort(), opts.timeout ?? TIMEOUT_MS.default)
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
method,
|
|
33
|
+
signal: ctrl.signal,
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
// 身份注入:数据范围按建页人 union_id 在 dataclaw 侧裁剪 + 服务间共享密钥校验。
|
|
37
|
+
'X-User-UnionId': this.cfg.asUnionId,
|
|
38
|
+
'X-Internal-Secret': this.cfg.internalSecret,
|
|
39
|
+
},
|
|
40
|
+
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
41
|
+
})
|
|
42
|
+
const text = await res.text()
|
|
43
|
+
let json: any
|
|
44
|
+
try {
|
|
45
|
+
json = text ? JSON.parse(text) : {}
|
|
46
|
+
} catch {
|
|
47
|
+
throw new HttpException({ ok: false, error: `dataclaw 非 JSON 响应: ${text.slice(0, 200)}` }, 502)
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok || json?.ok === false) {
|
|
50
|
+
// HTTP 非 2xx → 映射上游状态(5xx 归 502);HTTP 2xx 但应用层 {ok:false} → 502
|
|
51
|
+
// (别把 200 当错误码抛——对前端是"成功码带错误体"的矛盾态)。
|
|
52
|
+
const status = res.ok ? 502 : res.status >= 500 ? 502 : res.status || 400
|
|
53
|
+
throw new HttpException(
|
|
54
|
+
{ ok: false, error: json?.error || `dataclaw ${res.status}`, upstreamStatus: res.status },
|
|
55
|
+
status,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
return json as T
|
|
59
|
+
} catch (err: any) {
|
|
60
|
+
if (err instanceof HttpException) throw err
|
|
61
|
+
if (err?.name === 'AbortError') throw new HttpException({ ok: false, error: 'dataclaw 请求超时' }, 504)
|
|
62
|
+
throw new HttpException({ ok: false, error: `dataclaw 连接失败: ${err?.message}` }, 502)
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(t)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getSchema(dataset?: string) {
|
|
69
|
+
return this.call<any>('GET', '/api/schema', { query: { dataset } })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
flexibleQuery(body: unknown) {
|
|
73
|
+
return this.call<any>('POST', '/api/flexible-query', { body })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
intentExecute(body: unknown) {
|
|
77
|
+
return this.call<any>('POST', '/api/intent-execute', { body, timeout: TIMEOUT_MS.intent })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
consultants(keyword: string) {
|
|
81
|
+
return this.call<any>('GET', '/api/consultants', { query: { keyword } })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface DataclawConfig {
|
|
2
|
+
baseUrl: string
|
|
3
|
+
asUnionId: string
|
|
4
|
+
internalSecret: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function loadDataclawConfig(): DataclawConfig {
|
|
8
|
+
const baseUrl = (process.env.DATACLAW_SERVICE_URL || '').replace(/\/+$/, '')
|
|
9
|
+
const asUnionId = process.env.DATACLAW_AS_UNION_ID || ''
|
|
10
|
+
const internalSecret = process.env.DATACLAW_INTERNAL_SECRET || ''
|
|
11
|
+
const missing = [
|
|
12
|
+
!baseUrl && 'DATACLAW_SERVICE_URL',
|
|
13
|
+
!asUnionId && 'DATACLAW_AS_UNION_ID',
|
|
14
|
+
!internalSecret && 'DATACLAW_INTERNAL_SECRET',
|
|
15
|
+
].filter(Boolean)
|
|
16
|
+
if (missing.length) throw new Error(`[dataclaw] 缺少必需 env: ${missing.join(', ')}`)
|
|
17
|
+
return { baseUrl, asUnionId, internalSecret }
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Body, Controller, Get, Post, Query } from '@nestjs/common'
|
|
2
|
+
import { DataclawClient } from './dataclaw.client'
|
|
3
|
+
|
|
4
|
+
// app 自己的只读 BFF。main.ts 已 setGlobalPrefix('api' 或子路径 'preview/<port>/api'),
|
|
5
|
+
// 故这里的路径不带 /api。
|
|
6
|
+
// 登录门禁由 main.ts 的 Express authGate 中间件统一覆盖(含 /api/*),不再用 Nest guard。
|
|
7
|
+
@Controller()
|
|
8
|
+
export class DataclawController {
|
|
9
|
+
constructor(private readonly dc: DataclawClient) {}
|
|
10
|
+
|
|
11
|
+
@Get('schema')
|
|
12
|
+
schema(@Query('dataset') dataset?: string) {
|
|
13
|
+
return this.dc.getSchema(dataset)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Post('query')
|
|
17
|
+
query(@Body() body: unknown) {
|
|
18
|
+
return this.dc.flexibleQuery(body)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Post('intent')
|
|
22
|
+
intent(@Body() body: unknown) {
|
|
23
|
+
return this.dc.intentExecute(body)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Get('consultants')
|
|
27
|
+
consultants(@Query('keyword') keyword = '') {
|
|
28
|
+
return this.dc.consultants(keyword)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common'
|
|
2
|
+
import { DataclawClient } from './dataclaw.client'
|
|
3
|
+
import { DataclawController } from './dataclaw.controller'
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
controllers: [DataclawController],
|
|
7
|
+
providers: [DataclawClient],
|
|
8
|
+
})
|
|
9
|
+
export class DataclawModule {}
|