@clawos-dev/clawd 0.2.125 → 0.2.126-beta.252.5b08fb0
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/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/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/web/index.html +1 -1
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +159 -9
- package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/publish.sh +1 -8
- package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/remove-extension.sh +28 -19
- package/package.json +1 -1
|
@@ -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 {}
|
|
@@ -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,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
|
}
|
|
@@ -86,14 +86,7 @@ if [ -n "$DOM" ]; then
|
|
|
86
86
|
# 整体失败 → daemon runner 走 publish-failed 路径 + 回灌 chat 让 assistant 接管修复。
|
|
87
87
|
# 之前的 `|| true` 兜底已去掉,理由:线上挂了 prodUrl 仍写盘 = 老板看到"打开线上"按钮
|
|
88
88
|
# 但点开 404,不如 fail-loud。
|
|
89
|
-
|
|
90
|
-
# 显式走 bash 解释器调用(不依赖 verify.sh 自己的 +x 权限):
|
|
91
|
-
# daemon 调 publish.sh / new-extension.sh 都是 `spawn('bash', [scriptPath, ...])`,所以即使脚本
|
|
92
|
-
# +x bit 丢了(历史上有过:OTA 解包 / cp -p 未带 / desktop 旧包 mode 不对)也能跑。但这一行
|
|
93
|
-
# 之前是 direct-exec `"$KIT_DIR/scripts/verify.sh"`,OS 会检查 +x —— verify.sh 因此成为唯一
|
|
94
|
-
# 暴露 +x 丢失问题的执行点(老板撞到过一次"verify.sh permission denied")。
|
|
95
|
-
# 改 bash 调用后永远不再依赖 fs +x bit。
|
|
96
|
-
bash "$KIT_DIR/scripts/verify.sh" "http://$DOM"
|
|
89
|
+
"$KIT_DIR/scripts/verify.sh" "http://$DOM"
|
|
97
90
|
else
|
|
98
91
|
# 域名没拿到 = 部署本身有问题(fc3-domain 未成功),也算失败
|
|
99
92
|
echo "❌ 部署完成但未取到自定义域名,检查 fc3-domain 是否成功" >&2
|
|
@@ -24,34 +24,43 @@ set -a; . "$PERSONA_DIR/.secrets/aliyun.env"; set +a
|
|
|
24
24
|
echo "==> 删 FC 资源: $APP_NAME"
|
|
25
25
|
( cd "$EXT_DIR" && s remove -y )
|
|
26
26
|
|
|
27
|
-
# 2)
|
|
27
|
+
# 2) 列出要删的 Supabase 表(脚本不直接调 DB,改打印 DROP 让 agent 用 supabase MCP 跑)
|
|
28
|
+
#
|
|
29
|
+
# 历史 bug(2026-06-03 老板手测 + persona-app-builder 自查发现):旧实现走 python3 读
|
|
30
|
+
# .mcp.json 取 SUPABASE_ACCESS_TOKEN + curl https://api.supabase.com/v1/... 删表,三个
|
|
31
|
+
# 错叠在一起:
|
|
32
|
+
# ① 当前 .mcp.json 用阿里云 RDS 风格(--aliyun-ak 命令行参数),没有 env.SUPABASE_ACCESS_TOKEN
|
|
33
|
+
# 字段 → python3 KeyError → set -euo pipefail exit
|
|
34
|
+
# ② https://api.supabase.com 是云上 supabase.com 的 management API,跟阿里云自建实例
|
|
35
|
+
# 120.26.157.138 不通(即便通,删的也是云上早废弃的表)
|
|
36
|
+
# ③ SUPABASE_PROJECT_REF 是云上 project-ref 概念,阿里云自建用 instance_name
|
|
37
|
+
# 结果:第 1 步删 FC 一直对,第 2 步从来没真删过表(脚本提前 crash exit)。
|
|
38
|
+
#
|
|
39
|
+
# 修法:bash 调不到 MCP(MCP 是 LLM 通道),pg 直连要密码 agent 拿不到,PostgREST RPC 又
|
|
40
|
+
# 要装 drop_table_safe + 手刷 schema cache。最干净:打印 DROP,让 agent 看到 Bash tool
|
|
41
|
+
# stdout 后用 mcp__supabase__execute_sql 接力。长期方案是参照 publish.sh 模式做
|
|
42
|
+
# appBuilder.delete RPC + UI 按钮,等下一轮做。
|
|
28
43
|
if [ -n "$SUPABASE_TABLES" ]; then
|
|
29
|
-
echo ""
|
|
30
|
-
echo "⚠️ 即将删除以下 Supabase 表(数据不可逆):"
|
|
31
|
-
for t in $SUPABASE_TABLES; do echo " - public.$t"; done
|
|
32
|
-
|
|
33
44
|
# 安全锁:每个表必须 <app>_ 前缀,否则拒绝(防误伤 clawos 共享库)
|
|
34
45
|
for t in $SUPABASE_TABLES; do
|
|
35
46
|
case "$t" in
|
|
36
47
|
${APP_NAME}_*) ;;
|
|
37
|
-
*) echo "❌ 表 '$t' 不是 '${APP_NAME}_'
|
|
48
|
+
*) echo "❌ 表 '$t' 不是 '${APP_NAME}_' 前缀,拒绝输出(可能误伤 clawos)"; exit 1 ;;
|
|
38
49
|
esac
|
|
39
50
|
done
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
echo "跳过删表(FC 已删;表保留)。"
|
|
52
|
-
fi
|
|
52
|
+
echo ""
|
|
53
|
+
echo "==> ⚠️ 下面是要删的 Supabase 表,脚本不直接调 DB"
|
|
54
|
+
echo " (阿里云自建无 management API + bash 调不到 MCP)"
|
|
55
|
+
echo ""
|
|
56
|
+
for t in $SUPABASE_TABLES; do
|
|
57
|
+
echo " DROP TABLE IF EXISTS public.$t CASCADE;"
|
|
58
|
+
done
|
|
59
|
+
echo ""
|
|
60
|
+
echo "==> agent: 上面这几条 DROP 请用 supabase MCP 跑(mcp__supabase__execute_sql),"
|
|
61
|
+
echo " 完了项目清理才算完成"
|
|
53
62
|
else
|
|
54
|
-
echo "ext.conf 未声明 SUPABASE_TABLES
|
|
63
|
+
echo "ext.conf 未声明 SUPABASE_TABLES,无表要清理。"
|
|
55
64
|
fi
|
|
56
65
|
|
|
57
66
|
echo "✅ $APP_NAME 清理完成。"
|
package/package.json
CHANGED