@clawos-dev/clawd 0.2.124-beta.246.7490f60 → 0.2.124-beta.248.02a0096
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/persona-defaults/persona-app-builder/CLAUDE.md +5 -2
- package/dist/persona-defaults/persona-app-builder/extension-kit/config.env +8 -5
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.env.example +5 -2
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/package.json +1 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +5 -8
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +12 -2
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.controller.ts +27 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.module.ts +9 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.service.ts +49 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +7 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/index.html +1 -1
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +159 -9
- package/package.json +1 -1
|
@@ -163,10 +163,13 @@ createProject 成功后 daemon 自动:
|
|
|
163
163
|
|
|
164
164
|
## 后端:supabase MCP
|
|
165
165
|
|
|
166
|
-
**`supabase`** —— 后端数据/认证/存储,唯一的 MCP 支柱。这份 Supabase
|
|
166
|
+
**`supabase`** —— 后端数据/认证/存储,唯一的 MCP 支柱。这份 Supabase **是阿里云自建实例**(`http://120.26.157.138:80`),全局 MCP / clawos / lovagent / moltoffer 都共用这一台。**不是**云上 `supabase.com` 的实例。开工前确认能连上:
|
|
167
167
|
|
|
168
168
|
- 读表结构、跑 SQL、看 auth 用户都走这个 MCP,不要凭记忆猜 schema
|
|
169
|
-
-
|
|
169
|
+
- 模板里 `extension-kit/config.env` + `server/.env.example` 已对齐这台阿里云自建实例(SUPABASE_URL / SUPABASE_KEY),创建 project 时拷进项目目录的 `.env` 也是同一套
|
|
170
|
+
- **红线**:这是共享生产库。建表 / 迁移前先 `list_tables` 看清现状,新项目的表**必须**用 `<项目名>_` 前缀沉到 `public` schema(如 `helloworld_click_counter`),避免和 clawos / 其它项目的表撞名;**绝不** drop / alter clawos 已有的表,不确定哪些是 clawos 的就先问老板
|
|
171
|
+
|
|
172
|
+
**易踩的坑**:MCP 工具走阿里云自建那台,假如哪一步把 `.env` / `config.env` 写成云上 `qmhqmopgwsbmfxiqagay.supabase.co`(曾经的旧默认),就会**MCP 建表落阿里云 / server 通过 supabase-js 查云上**,两台 PostgREST schema cache 各自独立 → `PGRST205 schema cache 找不到表`。看到 PGRST 系列错码先 `grep -r supabase.co` 自查模板配置,再怀疑 schema cache。
|
|
170
173
|
|
|
171
174
|
## 阿里云凭证:在 `.secrets/aliyun.env`,用时现读
|
|
172
175
|
|
|
@@ -8,11 +8,14 @@ REGION=cn-hangzhou
|
|
|
8
8
|
FC_RUNTIME=custom.debian12
|
|
9
9
|
NODE_LAYER=acs:fc:cn-hangzhou:official:layers/Nodejs22/versions/1
|
|
10
10
|
|
|
11
|
-
# Supabase(clawos
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
|
|
11
|
+
# Supabase(clawos 共享阿里云自建实例!建表务必加 <app>_ 前缀沉到 public schema,绝不动 clawos 已有表)
|
|
12
|
+
# 注意:这是阿里云自建 supabase(120.26.157.138),不是云上 supabase.com 实例 ——
|
|
13
|
+
# 全局 MCP / clawos / lovagent / moltoffer 都共用这一台,跟 supabase MCP 操作的也是这台。
|
|
14
|
+
# 之前误指云上 qmhqmopgwsbmfxiqagay.supabase.co 导致 MCP 建表落阿里云,server 走 supabase-js
|
|
15
|
+
# 查云上,两台 PostgREST schema cache 独立 → PGRST205 找不到表。
|
|
16
|
+
SUPABASE_URL=http://120.26.157.138:80
|
|
17
|
+
# anon/publishable key(公开可暴露,客户端用)。阿里云自建实例无 project-ref 概念。
|
|
18
|
+
SUPABASE_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNzY3OTU0MTY0LCJleHAiOjEzMjc4NTk0MTY0fQ.quOr1bQZ2OCQ49_UCto8LGY-n90Hlku1IbJYltFVWPc
|
|
16
19
|
|
|
17
20
|
# FC 资源规格(够跑轻量 extension;按需改)
|
|
18
21
|
FC_CPU=0.35
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# Supabase 共享阿里云自建实例(跟 extension-kit/config.env 对齐)。
|
|
2
|
+
# 这台 supabase 也是全局 MCP / clawos / lovagent / moltoffer 在用的同一台 ——
|
|
3
|
+
# 用 MCP 建表落到这里,server 通过 supabase-js 读也走这里,schema cache 对得齐。
|
|
4
|
+
SUPABASE_URL=http://120.26.157.138:80
|
|
5
|
+
SUPABASE_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNzY3OTU0MTY0LCJleHAiOjEzMjc4NTk0MTY0fQ.quOr1bQZ2OCQ49_UCto8LGY-n90Hlku1IbJYltFVWPc
|
|
3
6
|
PORT=3000
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { Module } from '@nestjs/common';
|
|
2
|
+
import { MessagesModule } from './messages/messages.module';
|
|
2
3
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// 瘦身后只留启动骨架,业务逻辑由 assistant 按用户需求从零加(添 controller/module 进 imports 即可)。
|
|
7
|
-
//
|
|
8
|
-
// vite middleware / 静态资源 serve 路径仍由 main.ts 处理(dev mount vite,prod express.static),
|
|
9
|
-
// 不需要 ServeStaticModule。
|
|
4
|
+
// dev 模式:main.ts mount vite middleware 处理非 /api 路径(含 HMR);
|
|
5
|
+
// prod 模式:main.ts express.static serve build 后的 web/dist。
|
|
6
|
+
// 两种模式都不用 ServeStaticModule(vite middleware 跟它会撞),就近在 main.ts 处理。
|
|
10
7
|
@Module({
|
|
11
|
-
imports: [],
|
|
8
|
+
imports: [MessagesModule],
|
|
12
9
|
})
|
|
13
10
|
export class AppModule {}
|
|
@@ -56,10 +56,20 @@ async function bootstrap() {
|
|
|
56
56
|
allowedHosts,
|
|
57
57
|
},
|
|
58
58
|
// appType: 'spa' 让 vite middleware 自己 serve index.html + fallback 路由(base 下任何
|
|
59
|
-
// 非 vite asset 路径都返 index.html)。
|
|
59
|
+
// 非 vite asset 路径都返 index.html)。
|
|
60
60
|
appType: 'spa',
|
|
61
61
|
});
|
|
62
|
-
|
|
62
|
+
// /api/* 守卫:实测 vite 'spa' catch-all 会先吃掉 /api/* 返 HTML,导致 API 拿不到 JSON。
|
|
63
|
+
// Express middleware 栈顺序:vite.middlewares 这一行加进去时 nest controllers 还没挂
|
|
64
|
+
// (nest router init 在 app.listen 时),所以 vite 拦截优先于 nest router → 命中 /api 也走 vite。
|
|
65
|
+
// 守卫:req.url 命中 apiPrefix 时直接 next() 跳过 vite,让 nest router 接管返 JSON。
|
|
66
|
+
const apiPathPrefix = `/${apiPrefix}/`; // 如 '/preview/6173/api/' 或 '/api/'
|
|
67
|
+
app.use((req: any, res: any, next: any) => {
|
|
68
|
+
if (typeof req.url === 'string' && req.url.startsWith(apiPathPrefix)) {
|
|
69
|
+
return next(); // skip vite,让 nest router 处理 /api/*
|
|
70
|
+
}
|
|
71
|
+
return vite.middlewares(req, res, next);
|
|
72
|
+
});
|
|
63
73
|
console.log(`[clawd-dev] vite middleware mounted (root=${viteRoot}, base=${viteBase}, apiPrefix=${apiPrefix}, allowedHosts=${allowedHosts.join(',')})`);
|
|
64
74
|
} else {
|
|
65
75
|
// prod / 脱离 clawd 跑:直接 serve 前端静态资源。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Get,
|
|
6
|
+
Post,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { MessagesService } from './messages.service';
|
|
9
|
+
|
|
10
|
+
@Controller('messages')
|
|
11
|
+
export class MessagesController {
|
|
12
|
+
constructor(private readonly messages: MessagesService) {}
|
|
13
|
+
|
|
14
|
+
@Get()
|
|
15
|
+
list() {
|
|
16
|
+
return this.messages.list();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Post()
|
|
20
|
+
create(@Body() body: { name?: string; content?: string }) {
|
|
21
|
+
const content = body?.content?.trim();
|
|
22
|
+
if (!content) {
|
|
23
|
+
throw new BadRequestException('content 不能为空');
|
|
24
|
+
}
|
|
25
|
+
return this.messages.create(body.name?.trim() ?? '', content);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { MessagesController } from './messages.controller';
|
|
3
|
+
import { MessagesService } from './messages.service';
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
controllers: [MessagesController],
|
|
7
|
+
providers: [MessagesService],
|
|
8
|
+
})
|
|
9
|
+
export class MessagesModule {}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
|
|
4
|
+
const TABLE = 'guestbook_messages';
|
|
5
|
+
|
|
6
|
+
// 模板示例 service。Supabase client **lazy 初始化**:
|
|
7
|
+
// - constructor 不读 env / 不 throw —— 让整个 nest server 能起来即使 SUPABASE_URL/KEY 没配
|
|
8
|
+
// (比如 fresh project 还没接 Supabase,老板只想看个 welcome 页面)
|
|
9
|
+
// - 实际调 list / create 时再 require,缺 env 时只报给这条 API 的 caller,不阻塞 server 启动
|
|
10
|
+
//
|
|
11
|
+
// 老 bug:constructor 里硬 require + throw → 没配 env 时 NestApplication 启动直接 crash,
|
|
12
|
+
// dev server 进 exit 1 → preview iframe upstream error 糊脸。
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class MessagesService {
|
|
15
|
+
private _supabase: SupabaseClient | null = null;
|
|
16
|
+
|
|
17
|
+
private get supabase(): SupabaseClient {
|
|
18
|
+
if (this._supabase) return this._supabase;
|
|
19
|
+
const url = process.env.SUPABASE_URL;
|
|
20
|
+
const key = process.env.SUPABASE_KEY;
|
|
21
|
+
if (!url || !key) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'SUPABASE_URL / SUPABASE_KEY 未配置 —— 请先接 Supabase 后端再调 /api/messages',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
this._supabase = createClient(url, key);
|
|
27
|
+
return this._supabase;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async list() {
|
|
31
|
+
const { data, error } = await this.supabase
|
|
32
|
+
.from(TABLE)
|
|
33
|
+
.select('*')
|
|
34
|
+
.order('created_at', { ascending: false })
|
|
35
|
+
.limit(100);
|
|
36
|
+
if (error) throw new Error(`读取留言失败: ${error.message}`);
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async create(name: string, content: string) {
|
|
41
|
+
const { data, error } = await this.supabase
|
|
42
|
+
.from(TABLE)
|
|
43
|
+
.insert({ name: name || 'anonymous', content })
|
|
44
|
+
.select()
|
|
45
|
+
.single();
|
|
46
|
+
if (error) throw new Error(`写入留言失败: ${error.message}`);
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Dev 模式 .env loader:`nest start --watch` 不会经过 `node --env-file=.env`,
|
|
2
|
+
// server 拿不到 SUPABASE_URL/KEY → createClient 跑了但 process.env 是空 → PGRST 系列错码。
|
|
3
|
+
// 这里在 nest bootstrap 之前显式 require dotenv 把 .env 填进 process.env。
|
|
4
|
+
// prod 路径(node --env-file=.env dist/main.js)Node 自带 loader 已经填了,dotenv 看到
|
|
5
|
+
// 已存在的 env 不会覆盖(默认行为),行为对齐。
|
|
6
|
+
require('dotenv/config');
|
|
7
|
+
|
|
1
8
|
// Node < 22 没有原生 WebSocket,而 @supabase/supabase-js 的 realtime 客户端
|
|
2
9
|
// 初始化时需要它。在应用启动前补一个全局 WebSocket,使任意 Node 版本
|
|
3
10
|
// (FC 运行时给的 node 可能是 18/20)下都能正常 createClient。
|
|
@@ -1,11 +1,161 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const styles = {
|
|
4
|
+
page: {
|
|
5
|
+
minHeight: '100vh',
|
|
6
|
+
margin: 0,
|
|
7
|
+
background: 'linear-gradient(160deg, #0f172a 0%, #1e293b 100%)',
|
|
8
|
+
color: '#e2e8f0',
|
|
9
|
+
fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
10
|
+
display: 'flex',
|
|
11
|
+
justifyContent: 'center',
|
|
12
|
+
padding: '48px 16px',
|
|
13
|
+
},
|
|
14
|
+
card: {
|
|
15
|
+
width: '100%',
|
|
16
|
+
maxWidth: 560,
|
|
17
|
+
},
|
|
18
|
+
title: { fontSize: 28, fontWeight: 700, margin: '0 0 4px' },
|
|
19
|
+
subtitle: { color: '#94a3b8', margin: '0 0 24px', fontSize: 14 },
|
|
20
|
+
form: { display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 },
|
|
21
|
+
row: { display: 'flex', gap: 10 },
|
|
22
|
+
input: {
|
|
23
|
+
flex: 1,
|
|
24
|
+
padding: '10px 12px',
|
|
25
|
+
borderRadius: 8,
|
|
26
|
+
border: '1px solid #334155',
|
|
27
|
+
background: '#0f172a',
|
|
28
|
+
color: '#e2e8f0',
|
|
29
|
+
fontSize: 14,
|
|
30
|
+
outline: 'none',
|
|
31
|
+
},
|
|
32
|
+
textarea: {
|
|
33
|
+
padding: '10px 12px',
|
|
34
|
+
borderRadius: 8,
|
|
35
|
+
border: '1px solid #334155',
|
|
36
|
+
background: '#0f172a',
|
|
37
|
+
color: '#e2e8f0',
|
|
38
|
+
fontSize: 14,
|
|
39
|
+
outline: 'none',
|
|
40
|
+
resize: 'vertical',
|
|
41
|
+
minHeight: 64,
|
|
42
|
+
},
|
|
43
|
+
button: {
|
|
44
|
+
padding: '10px 20px',
|
|
45
|
+
borderRadius: 8,
|
|
46
|
+
border: 'none',
|
|
47
|
+
background: '#6366f1',
|
|
48
|
+
color: '#fff',
|
|
49
|
+
fontSize: 14,
|
|
50
|
+
fontWeight: 600,
|
|
51
|
+
cursor: 'pointer',
|
|
52
|
+
},
|
|
53
|
+
msg: {
|
|
54
|
+
padding: '12px 14px',
|
|
55
|
+
borderRadius: 10,
|
|
56
|
+
background: '#1e293b',
|
|
57
|
+
border: '1px solid #334155',
|
|
58
|
+
marginBottom: 10,
|
|
59
|
+
},
|
|
60
|
+
msgHead: { display: 'flex', justifyContent: 'space-between', marginBottom: 4 },
|
|
61
|
+
msgName: { fontWeight: 600, color: '#a5b4fc', fontSize: 14 },
|
|
62
|
+
msgTime: { color: '#64748b', fontSize: 12 },
|
|
63
|
+
msgBody: { fontSize: 14, lineHeight: 1.5, whiteSpace: 'pre-wrap' },
|
|
64
|
+
error: { color: '#f87171', fontSize: 13, marginBottom: 12 },
|
|
65
|
+
empty: { color: '#64748b', fontSize: 14, textAlign: 'center', padding: '24px 0' },
|
|
66
|
+
};
|
|
67
|
+
|
|
9
68
|
export default function App() {
|
|
10
|
-
|
|
69
|
+
const [messages, setMessages] = useState([]);
|
|
70
|
+
const [name, setName] = useState('');
|
|
71
|
+
const [content, setContent] = useState('');
|
|
72
|
+
const [error, setError] = useState('');
|
|
73
|
+
const [loading, setLoading] = useState(false);
|
|
74
|
+
|
|
75
|
+
async function load() {
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch(`${import.meta.env.BASE_URL}api/messages`);
|
|
78
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
79
|
+
setMessages(await res.json());
|
|
80
|
+
setError('');
|
|
81
|
+
} catch (e) {
|
|
82
|
+
setError(`加载失败: ${e.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
load();
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
async function submit(e) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
if (!content.trim()) return;
|
|
93
|
+
setLoading(true);
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(`${import.meta.env.BASE_URL}api/messages`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ name, content }),
|
|
99
|
+
});
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
const body = await res.json().catch(() => ({}));
|
|
102
|
+
throw new Error(body.message || `HTTP ${res.status}`);
|
|
103
|
+
}
|
|
104
|
+
setContent('');
|
|
105
|
+
await load();
|
|
106
|
+
} catch (e) {
|
|
107
|
+
setError(`提交失败: ${e.message}`);
|
|
108
|
+
} finally {
|
|
109
|
+
setLoading(false);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div style={styles.page}>
|
|
115
|
+
<div style={styles.card}>
|
|
116
|
+
<h1 style={styles.title}>留言板</h1>
|
|
117
|
+
<p style={styles.subtitle}>
|
|
118
|
+
前端 React → NestJS → Supabase 全链路连通性测试
|
|
119
|
+
</p>
|
|
120
|
+
|
|
121
|
+
<form style={styles.form} onSubmit={submit}>
|
|
122
|
+
<input
|
|
123
|
+
style={styles.input}
|
|
124
|
+
placeholder="你的名字(可选)"
|
|
125
|
+
value={name}
|
|
126
|
+
onChange={(e) => setName(e.target.value)}
|
|
127
|
+
/>
|
|
128
|
+
<textarea
|
|
129
|
+
style={styles.textarea}
|
|
130
|
+
placeholder="写点什么…"
|
|
131
|
+
value={content}
|
|
132
|
+
onChange={(e) => setContent(e.target.value)}
|
|
133
|
+
/>
|
|
134
|
+
<div style={styles.row}>
|
|
135
|
+
<button style={styles.button} type="submit" disabled={loading}>
|
|
136
|
+
{loading ? '提交中…' : '提交留言'}
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
</form>
|
|
140
|
+
|
|
141
|
+
{error && <div style={styles.error}>{error}</div>}
|
|
142
|
+
|
|
143
|
+
{messages.length === 0 ? (
|
|
144
|
+
<div style={styles.empty}>还没有留言,来写第一条吧</div>
|
|
145
|
+
) : (
|
|
146
|
+
messages.map((m) => (
|
|
147
|
+
<div key={m.id} style={styles.msg}>
|
|
148
|
+
<div style={styles.msgHead}>
|
|
149
|
+
<span style={styles.msgName}>{m.name}</span>
|
|
150
|
+
<span style={styles.msgTime}>
|
|
151
|
+
{new Date(m.created_at).toLocaleString('zh-CN')}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
<div style={styles.msgBody}>{m.content}</div>
|
|
155
|
+
</div>
|
|
156
|
+
))
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
11
161
|
}
|
package/package.json
CHANGED