@clawos-dev/clawd 0.2.112-beta.217.80b79a2 → 0.2.112-beta.220.a78f1c0
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 +1125 -1070
- package/dist/persona-defaults/persona-app-builder/CLAUDE.md +168 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/README.md +96 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/config.env +20 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/contract/bootstrap +22 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/contract/s.yaml.tmpl +54 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.env.example +3 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.fcignore +7 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/nest-cli.json +8 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/package-lock.json +4531 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/package.json +28 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +10 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +49 -0
- 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 +38 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +8 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/tsconfig.json +14 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/index.html +12 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/package-lock.json +1680 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/package.json +18 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +161 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/main.jsx +5 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/vite.config.js +30 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/new-extension.sh +49 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/publish.sh +78 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/remove-extension.sh +57 -0
- package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/verify.sh +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "guestbook-web",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"react": "^18.3.1",
|
|
12
|
+
"react-dom": "^18.3.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
16
|
+
"vite": "^5.4.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
|
|
68
|
+
export default function App() {
|
|
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('/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('/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
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
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);
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
plugins: [react()],
|
|
15
|
+
build: {
|
|
16
|
+
outDir: 'dist',
|
|
17
|
+
emptyOutDir: true,
|
|
18
|
+
},
|
|
19
|
+
// 隧道经 nest 主进程进来,vite middleware 自己识别 base。standalone 模式(无 tunnel)
|
|
20
|
+
// 走 base: '/' 方便本地直接打开。
|
|
21
|
+
base: tunnelHost ? `/preview/${port}/` : '/',
|
|
22
|
+
server: {
|
|
23
|
+
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,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 从示例模板生成一个新 extension
|
|
3
|
+
# 用法: new-extension.sh <name> [示例模板, 默认 nestjs-react]
|
|
4
|
+
#
|
|
5
|
+
# 目标目录解析:
|
|
6
|
+
# - picker 模式(推荐):persona-app-builder/projects/<name>/ 已由 daemon createProject
|
|
7
|
+
# 建好(含 .clawd-project.json)→ 把模板内容展开到此目录,保留 .clawd-project.json。
|
|
8
|
+
# - 老路径兼容:projects/<name>/ 不存在时回退到 persona-app-builder/<name>/。
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
11
|
+
PERSONA_DIR="$(cd "$KIT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
NAME="${1:?用法: new-extension.sh <name> [模板,默认 nestjs-react]}"
|
|
14
|
+
TMPL="${2:-nestjs-react}"
|
|
15
|
+
SRC="$KIT_DIR/examples/$TMPL"
|
|
16
|
+
|
|
17
|
+
[ -d "$SRC" ] || { echo "示例模板不存在: $SRC (现有: $(ls "$KIT_DIR/examples"))"; exit 1; }
|
|
18
|
+
|
|
19
|
+
PICKER_DEST="$PERSONA_DIR/projects/$NAME"
|
|
20
|
+
if [ -d "$PICKER_DEST" ] && [ -f "$PICKER_DEST/.clawd-project.json" ]; then
|
|
21
|
+
DEST="$PICKER_DEST"
|
|
22
|
+
# picker 模式:目录已存在且仅含 .clawd-project.json,把模板内容展开进去
|
|
23
|
+
REMAINING="$(find "$DEST" -mindepth 1 -maxdepth 1 ! -name '.clawd-project.json' -print -quit)"
|
|
24
|
+
[ -z "$REMAINING" ] || { echo "目标已有内容(非空目录): $DEST"; exit 1; }
|
|
25
|
+
cp -R "$SRC/." "$DEST/"
|
|
26
|
+
else
|
|
27
|
+
DEST="$PERSONA_DIR/$NAME"
|
|
28
|
+
[ -e "$DEST" ] && { echo "目标已存在: $DEST"; exit 1; }
|
|
29
|
+
cp -r "$SRC" "$DEST"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# 写 ext.conf(框架相关配置,publish.sh 会读)
|
|
33
|
+
cat > "$DEST/ext.conf" <<EOF
|
|
34
|
+
APP_NAME=$NAME
|
|
35
|
+
CODE_DIR=./server
|
|
36
|
+
FC_ENTRY=dist/main.js
|
|
37
|
+
BUILD_CMD="cd web && npm install && npm run build && cd ../server && npm install && npm run build"
|
|
38
|
+
# 本 extension 拥有的 Supabase 表(空格分隔,必须 ${NAME}_ 前缀);remove-extension.sh 据此清理
|
|
39
|
+
SUPABASE_TABLES=""
|
|
40
|
+
EOF
|
|
41
|
+
|
|
42
|
+
echo "✅ 新 extension: $DEST"
|
|
43
|
+
echo ""
|
|
44
|
+
echo "接下来:"
|
|
45
|
+
echo " 1) 设计 Supabase 表(务必用 ${NAME}_ 前缀,别碰 clawos 已有表)"
|
|
46
|
+
echo " 2) 改前端(web/)和后端(server/)业务逻辑"
|
|
47
|
+
echo " 3) 本地验证后发布: extension-kit/scripts/publish.sh $DEST"
|
|
48
|
+
echo ""
|
|
49
|
+
echo "换框架/语言: 改 ext.conf 的 BUILD_CMD/FC_ENTRY;换语言再看 extension-kit/README.md"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# 一键发布 extension 到阿里云 FC(框架无关)
|
|
4
|
+
# 用法: publish.sh <extension目录>
|
|
5
|
+
# extension 目录里可放 ext.conf 声明框架相关的: APP_NAME/CODE_DIR/FC_ENTRY/BUILD_CMD
|
|
6
|
+
# ============================================================
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
PERSONA_DIR="$(cd "$KIT_DIR/.." && pwd)"
|
|
11
|
+
|
|
12
|
+
EXT_DIR="${1:?用法: publish.sh <extension目录>}"
|
|
13
|
+
EXT_DIR="$(cd "$EXT_DIR" && pwd)"
|
|
14
|
+
|
|
15
|
+
# 1) 平台变量 + extension 自身配置(后者覆盖前者)
|
|
16
|
+
source "$KIT_DIR/config.env"
|
|
17
|
+
[ -f "$EXT_DIR/ext.conf" ] && source "$EXT_DIR/ext.conf"
|
|
18
|
+
APP_NAME="${APP_NAME:-$(basename "$EXT_DIR")}"
|
|
19
|
+
CODE_DIR="${CODE_DIR:-./server}"
|
|
20
|
+
FC_ENTRY="${FC_ENTRY:-dist/main.js}"
|
|
21
|
+
BUILD_CMD="${BUILD_CMD:-}"
|
|
22
|
+
|
|
23
|
+
# 2) 阿里云凭证(现读)+ Serverless Devs 认证(必须含 AccountID,SK 走 heredoc 不进命令行)
|
|
24
|
+
set -a; . "$PERSONA_DIR/.secrets/aliyun.env"; set +a
|
|
25
|
+
ACC="$(aliyun sts GetCallerIdentity 2>/dev/null | python3 -c 'import sys,json;print(json.load(sys.stdin)["AccountId"])')"
|
|
26
|
+
mkdir -p ~/.s
|
|
27
|
+
cat > ~/.s/access.yaml <<EOF
|
|
28
|
+
default:
|
|
29
|
+
AccountID: '$ACC'
|
|
30
|
+
AccessKeyID: $ALIBABA_CLOUD_ACCESS_KEY_ID
|
|
31
|
+
AccessKeySecret: $ALIBABA_CLOUD_ACCESS_KEY_SECRET
|
|
32
|
+
EOF
|
|
33
|
+
chmod 600 ~/.s/access.yaml
|
|
34
|
+
|
|
35
|
+
# 3) 构建产物(框架相关,由 ext.conf 的 BUILD_CMD 定义)
|
|
36
|
+
if [ -n "$BUILD_CMD" ]; then
|
|
37
|
+
echo "==> build: $BUILD_CMD"
|
|
38
|
+
( cd "$EXT_DIR" && eval "$BUILD_CMD" )
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# 4) 放置框架无关的 bootstrap 到 code 目录
|
|
42
|
+
cp "$KIT_DIR/contract/bootstrap" "$EXT_DIR/$CODE_DIR/bootstrap"
|
|
43
|
+
chmod +x "$EXT_DIR/$CODE_DIR/bootstrap"
|
|
44
|
+
|
|
45
|
+
# 5) 渲染 s.yaml(| 作分隔符,避免 URL/层 ARN 里的 / : 冲突)
|
|
46
|
+
sed -e "s|__APP_NAME__|$APP_NAME|g" \
|
|
47
|
+
-e "s|__REGION__|$REGION|g" \
|
|
48
|
+
-e "s|__FC_RUNTIME__|$FC_RUNTIME|g" \
|
|
49
|
+
-e "s|__NODE_LAYER__|$NODE_LAYER|g" \
|
|
50
|
+
-e "s|__FC_CPU__|$FC_CPU|g" \
|
|
51
|
+
-e "s|__FC_MEMORY__|$FC_MEMORY|g" \
|
|
52
|
+
-e "s|__FC_TIMEOUT__|$FC_TIMEOUT|g" \
|
|
53
|
+
-e "s|__CODE_DIR__|$CODE_DIR|g" \
|
|
54
|
+
-e "s|__FC_ENTRY__|$FC_ENTRY|g" \
|
|
55
|
+
-e "s|__SUPABASE_URL__|$SUPABASE_URL|g" \
|
|
56
|
+
-e "s|__SUPABASE_KEY__|$SUPABASE_KEY|g" \
|
|
57
|
+
"$KIT_DIR/contract/s.yaml.tmpl" > "$EXT_DIR/s.yaml"
|
|
58
|
+
|
|
59
|
+
# 6) 部署
|
|
60
|
+
echo "==> s deploy ($APP_NAME)"
|
|
61
|
+
( cd "$EXT_DIR" && s deploy -y )
|
|
62
|
+
|
|
63
|
+
# 7) 取自定义域名 + 验证
|
|
64
|
+
DOM="$(aliyun fc GET /2023-03-30/custom-domains --region "$REGION" 2>/dev/null | python3 -c "
|
|
65
|
+
import sys,json
|
|
66
|
+
d=json.load(sys.stdin)
|
|
67
|
+
m=[c['domainName'] for c in d.get('customDomains',[]) if '$APP_NAME' in c.get('domainName','')]
|
|
68
|
+
print(m[0] if m else '')")"
|
|
69
|
+
|
|
70
|
+
echo ""
|
|
71
|
+
echo "=================================================="
|
|
72
|
+
if [ -n "$DOM" ]; then
|
|
73
|
+
echo " ✅ 发布完成: http://$DOM"
|
|
74
|
+
echo "=================================================="
|
|
75
|
+
"$KIT_DIR/scripts/verify.sh" "http://$DOM" || true
|
|
76
|
+
else
|
|
77
|
+
echo " ⚠️ 部署完成但未取到自定义域名,检查 fc3-domain 是否成功"
|
|
78
|
+
fi
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# 清理一个 extension:删 FC 资源(函数+触发器+域名)+ 可选删它的 Supabase 表
|
|
4
|
+
# 用法: remove-extension.sh <extension目录>
|
|
5
|
+
# 删哪些表:读 ext.conf 的 SUPABASE_TABLES(extension 自己声明的)
|
|
6
|
+
# 安全:① 校验 <app>_ 前缀防误删 clawos ② 列出+二次确认 ③ 数据不可逆
|
|
7
|
+
# ============================================================
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
11
|
+
PERSONA_DIR="$(cd "$KIT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
EXT_DIR="${1:?用法: remove-extension.sh <extension目录>}"
|
|
14
|
+
EXT_DIR="$(cd "$EXT_DIR" && pwd)"
|
|
15
|
+
|
|
16
|
+
source "$KIT_DIR/config.env"
|
|
17
|
+
[ -f "$EXT_DIR/ext.conf" ] && source "$EXT_DIR/ext.conf"
|
|
18
|
+
APP_NAME="${APP_NAME:-$(basename "$EXT_DIR")}"
|
|
19
|
+
SUPABASE_TABLES="${SUPABASE_TABLES:-}"
|
|
20
|
+
|
|
21
|
+
set -a; . "$PERSONA_DIR/.secrets/aliyun.env"; set +a
|
|
22
|
+
|
|
23
|
+
# 1) 删 FC 侧(函数 + 触发器 + 自定义域名)
|
|
24
|
+
echo "==> 删 FC 资源: $APP_NAME"
|
|
25
|
+
( cd "$EXT_DIR" && s remove -y )
|
|
26
|
+
|
|
27
|
+
# 2) 删 Supabase 表(仅当 ext.conf 声明了 SUPABASE_TABLES)
|
|
28
|
+
if [ -n "$SUPABASE_TABLES" ]; then
|
|
29
|
+
echo ""
|
|
30
|
+
echo "⚠️ 即将删除以下 Supabase 表(数据不可逆):"
|
|
31
|
+
for t in $SUPABASE_TABLES; do echo " - public.$t"; done
|
|
32
|
+
|
|
33
|
+
# 安全锁:每个表必须 <app>_ 前缀,否则拒绝(防误伤 clawos 共享库)
|
|
34
|
+
for t in $SUPABASE_TABLES; do
|
|
35
|
+
case "$t" in
|
|
36
|
+
${APP_NAME}_*) ;;
|
|
37
|
+
*) echo "❌ 表 '$t' 不是 '${APP_NAME}_' 前缀,拒绝删除(可能误伤 clawos)"; exit 1 ;;
|
|
38
|
+
esac
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
read -r -p "确认删表? 输入 yes 继续: " ans
|
|
42
|
+
if [ "$ans" = "yes" ]; then
|
|
43
|
+
TOKEN="$(python3 -c "import json;print(json.load(open('$PERSONA_DIR/.mcp.json'))['mcpServers']['supabase']['env']['SUPABASE_ACCESS_TOKEN'])")"
|
|
44
|
+
for t in $SUPABASE_TABLES; do
|
|
45
|
+
curl -s -X POST "https://api.supabase.com/v1/projects/$SUPABASE_PROJECT_REF/database/query" \
|
|
46
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
47
|
+
-d "{\"query\":\"drop table if exists public.$t cascade;\"}" >/dev/null \
|
|
48
|
+
&& echo " ✓ dropped public.$t"
|
|
49
|
+
done
|
|
50
|
+
else
|
|
51
|
+
echo "跳过删表(FC 已删;表保留)。"
|
|
52
|
+
fi
|
|
53
|
+
else
|
|
54
|
+
echo "ext.conf 未声明 SUPABASE_TABLES,跳过删表。如有表请手动确认清理。"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
echo "✅ $APP_NAME 清理完成。"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 发布后健康检查:可达性 + Content-Type + 【是否被强制下载】
|
|
3
|
+
# 用法: verify.sh <url>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
URL="${1:?用法: verify.sh <url>}"
|
|
6
|
+
|
|
7
|
+
echo "== 健康检查: $URL =="
|
|
8
|
+
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "$URL/" || echo 000)
|
|
9
|
+
ctype=$(curl -s -o /dev/null -w "%{content_type}" --max-time 30 "$URL/" || echo "")
|
|
10
|
+
echo "首页: HTTP $code ($ctype)"
|
|
11
|
+
|
|
12
|
+
# 关键检查: 有 Content-Disposition 说明会被浏览器当文件下载(没绑自定义域名的典型症状)
|
|
13
|
+
hdr=$(curl -s -D - -o /dev/null --max-time 30 "$URL/" || true)
|
|
14
|
+
if echo "$hdr" | grep -qi 'content-disposition'; then
|
|
15
|
+
echo "❌ 检测到 Content-Disposition —— 浏览器会下载而非渲染!确认走的是 fc3-domain 自定义域名,而不是 fcapp.run"
|
|
16
|
+
else
|
|
17
|
+
echo "✓ 无 Content-Disposition,浏览器可正常渲染网页"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
[ "$code" = "200" ] && echo "✓ 全链路 OK" || echo "⚠️ 首页非 200,查 s logs"
|
package/package.json
CHANGED