@clawos-dev/clawd 0.2.454 → 0.2.455

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.
@@ -39,4 +39,29 @@ SLUG="$(openssl rand -hex 2)"
39
39
  echo "SLUG=${SLUG}"
40
40
  } >> "$DEST/ext.conf"
41
41
 
42
+ # 模板里的 *.env.example 只有 ${VAR} 引用没有值(凭据不随 OTA 落进 persona 目录),
43
+ # 这里用**本进程 env**(daemon spawn 时从凭据表注进来的,见 daemon/src/deploy/secret-table.ts)
44
+ # 展开成同目录的 .env,新项目 pnpm dev 就能直接跑。
45
+ # 通用实现(persona 无关):扫到哪个 ${VAR} 就取哪个同名变量,没有的渲成空串 + 提示,不 fail
46
+ # ——scaffold 不该因为一把 key 没配就整个失败,缺什么在 .env 里一眼看得出来。
47
+ render_env_example() {
48
+ local src="$1" dst="${1%.example}"
49
+ local sed_args=() ph var val
50
+ [ -f "$src" ] || return 0
51
+ [ -f "$dst" ] && return 0 # 已有 .env(用户自己写过)不覆盖
52
+ for ph in $(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*\}' "$src" | sort -u); do
53
+ var="${ph#\$\{}"; var="${var%\}}"
54
+ if declare -p "$var" >/dev/null 2>&1; then val="${!var}"; else
55
+ val=""
56
+ echo " ⚠️ $(basename "$src") 引用了 \$$var,但本次 scaffold 的环境里没有 → 渲成空串" >&2
57
+ fi
58
+ val="${val//\\/\\\\}"; val="${val//|/\\|}"; val="${val//&/\\&}"
59
+ # sed BRE 里 `{` `}` 是普通字符,`$` 用 [$] 转成字面量(\$ 后接 \{ 会被当区间量词)
60
+ sed_args+=(-e "s|[\$]{$var}|$val|g")
61
+ done
62
+ if [ ${#sed_args[@]} -eq 0 ]; then cp "$src" "$dst"; else sed "${sed_args[@]}" "$src" > "$dst"; fi
63
+ echo " 📄 渲染 ${dst#$DEST/}(值来自本次进程 env,不写回模板)"
64
+ }
65
+ while IFS= read -r f; do render_env_example "$f"; done < <(find "$DEST" -name '*.env.example' -not -path '*/node_modules/*')
66
+
42
67
  echo "✅ 新 extension: $DEST (APP_NAME=${NAME}, SLUG=${SLUG})"
@@ -59,14 +59,48 @@ cp "$KIT_DIR/contract/bootstrap" "$EXT_DIR/$CODE_DIR/bootstrap"
59
59
  chmod +x "$EXT_DIR/$CODE_DIR/bootstrap"
60
60
 
61
61
  # 6) 通用占位符替换渲染 s.yaml:扫 persona s.yaml.tmpl 的所有 __X__,从 env 取同名变量
62
+ #
63
+ # 自定义域名段按履约拼接(M1b Step 5c):APP_DOMAIN_SUFFIX 非空 = 部署级履约(域名与通配符
64
+ # 证书都在我们自己的阿里云账号上)→ 把 s.yaml.domain.tmpl 接上;空 = 使用者自带 key,
65
+ # 发布落在他自己的账号里,那里既没有我们的 DNS 也没有我们的证书 → 整段不渲染,用 FC 默认域名。
66
+ # 拼完再走同一套占位符替换,渲染契约本身不分叉。
62
67
  TMPL="$PERSONA_KIT/contract/s.yaml.tmpl"
63
68
  [ -f "$TMPL" ] || { echo "❌ 缺 $TMPL" >&2; exit 1; }
69
+ # **判据是「这个 persona 有没有 s.yaml.domain.tmpl」**,不是光看 APP_DOMAIN_SUFFIX:
70
+ # 没有那个文件的 persona(persona-dataclaw-builder)把自定义域名段写死在自己的 s.yaml.tmpl 里,
71
+ # 它压根不参与这套切换,替它选分支只会把它的发布路径改坏。
72
+ DOMAIN_TMPL="$PERSONA_KIT/contract/s.yaml.domain.tmpl"
73
+ TMPL_RENDER="$TMPL"
74
+ TMPL_COMBINED=""
75
+ if [ -f "$DOMAIN_TMPL" ]; then
76
+ if [ -n "${APP_DOMAIN_SUFFIX:-}" ]; then
77
+ # mktemp 落 EXT_DIR 之外:占位符缺失时下面直接 exit,别在用户项目目录里留残骸。
78
+ # **必须写全 XXXXXX 模板**:BSD mktemp(macOS)的 `-t prefix` 会自己补随机后缀,
79
+ # GNU coreutils(Linux,= M1b 的 Pod)要求模板以 ≥3 个 X 结尾,`mktemp -t s-yaml-tmpl`
80
+ # 直接 `too few X's in template` 退 1(coreutils 9.1 实测),`set -e` 下当场打死这条
81
+ # 默认路径。deploy-kit 的脚本两套 userland 都要跑,别用 BSD/GNU 语义有分歧的写法。
82
+ TMPL_COMBINED="$(mktemp "${TMPDIR:-/tmp}/s-yaml-tmpl.XXXXXX")"
83
+ trap 'rm -f "$TMPL_COMBINED"' EXIT
84
+ cat "$TMPL" "$DOMAIN_TMPL" > "$TMPL_COMBINED"
85
+ TMPL_RENDER="$TMPL_COMBINED"
86
+ echo " 🌐 自定义域名段:${DEPLOY_NAME}.${APP_DOMAIN_SUFFIX}"
87
+ else
88
+ echo " 🌐 未配 APP_DOMAIN_SUFFIX(使用者自带 key 履约):跳过自定义域名段,用 FC 默认域名"
89
+ fi
90
+ fi
64
91
  sed_args=()
65
- for ph in $(grep -oE '__[A-Z0-9_]+__' "$TMPL" | sort -u); do
92
+ for ph in $(grep -oE '__[A-Z0-9_]+__' "$TMPL_RENDER" | sort -u); do
66
93
  var="${ph#__}"; var="${var%__}"
67
94
  # declare -p 测「是否定义」(含空字符串),兼容 bash 3.2;未定义即 fail-loud
68
95
  if ! declare -p "$var" >/dev/null 2>&1; then
69
- echo "❌ s.yaml.tmpl 占位符 $ph 无对应 env 变量 \$$var(必填项缺失,或可选项未在 config.env/hook 里 export 兜底)" >&2
96
+ echo "❌ s.yaml.tmpl 占位符 $ph 无对应 env 变量 \$$var" >&2
97
+ # 最常见的成因不是「漏配」,而是**这个脚本被手动跑了**:凭据由 daemon 在 spawn 时注进
98
+ # 它起的那个子进程,别的地方起的 shell 拿不到。把这条写进报错里,是因为读它的下一个
99
+ # 读者往往是 agent——不给方向它就会去翻凭据、手动 export 或写回 config.env,
100
+ # 正好把「值不落 persona 目录」这条纪律拆掉。
101
+ echo " 如果你是手动跑这个脚本:凭据只注给 daemon 起的那个进程,手动跑必然缺。" >&2
102
+ echo " 正确做法是改调 appBuilder:publish RPC 重跑;**不要**去找 key 手动 export,也不要写回 config.env。" >&2
103
+ echo " 确实是漏配(新增了占位符 / 换了实例):改 daemon 的凭据表(~/.clawd/secrets/),不是改模板。" >&2
70
104
  exit 1
71
105
  fi
72
106
  # 转义 sed 替换串里的特殊字符(密钥/URL 可能含 \ | &),顺序:先 \ 再 | / &
@@ -76,7 +110,7 @@ for ph in $(grep -oE '__[A-Z0-9_]+__' "$TMPL" | sort -u); do
76
110
  val="${val//&/\\&}"
77
111
  sed_args+=(-e "s|$ph|$val|g")
78
112
  done
79
- sed "${sed_args[@]}" "$TMPL" > "$EXT_DIR/s.yaml"
113
+ sed "${sed_args[@]}" "$TMPL_RENDER" > "$EXT_DIR/s.yaml"
80
114
 
81
115
  # 7) deploy + verify
82
116
  echo "::stage::deploy"
@@ -84,23 +118,65 @@ echo "==> s deploy ($DEPLOY_NAME)"
84
118
  ( cd "$EXT_DIR" && s deploy -y --access "$S_ACCESS_ALIAS" )
85
119
 
86
120
  echo "::stage::verify"
87
- # 精确匹配 s.yaml.tmpl 里写死的 domainName 模式: <DEPLOY_NAME>.app.clawos.chat
88
- EXPECTED_DOM="${DEPLOY_NAME}.app.clawos.chat"
89
- DOM="$(aliyun_ak fc GET /2023-03-30/custom-domains 2>/dev/null | python3 -c "
90
- import sys,json
91
- d=json.load(sys.stdin)
92
- target='$EXPECTED_DOM'
93
- m=[c['domainName'] for c in d.get('customDomains',[]) if c.get('domainName','')==target]
94
- print(m[0] if m else '')")"
121
+ # 产物 URL 怎么取,取决于**这个 persona 参不参与 5c 的域名段方案**(判据同上面渲染那步,
122
+ # 不许各判各的):
123
+ # 有 s.yaml.domain.tmpl(app-builder):
124
+ # APP_DOMAIN_SUFFIX 非空 → 自定义域名,从 custom-domains 精确匹配
125
+ # APP_DOMAIN_SUFFIX 为空 → 使用者自带 key,FC 默认域名,从 httpTrigger 取
126
+ # 没有(persona-dataclaw-builder 等):自定义域名段写死在它自己的 s.yaml.tmpl 里,
127
+ # **原样走老路**——它的 functionName 规则、域名策略都跟这套无关,别替它选分支。
128
+ #
129
+ # JSON 解析用 node 不用 python3:目标机器保证有 node(clawd 本身就是 node 跑的),不保证有
130
+ # python。而且原先 `2>/dev/null | python3 -c` 把 CLI 的 stderr 吞了再让 python 抛
131
+ # JSONDecodeError,`set -e` 下脚本直接崩在 traceback 上,真凶(凭证不对 / 超时)全丢——
132
+ # 下面的 fc_get 保留 CLI 原文并 fail-loud。
133
+ fc_get() { # fc_get <path>:调 FC OpenAPI;失败把 CLI 原文写 stderr 后 return 1
134
+ local out rc
135
+ out="$(aliyun_ak fc GET "$1" 2>&1)" && rc=0 || rc=$?
136
+ if [ "$rc" -ne 0 ]; then
137
+ echo "❌ aliyun fc GET $1 失败,CLI 原文:" >&2
138
+ echo "$out" >&2
139
+ return 1
140
+ fi
141
+ printf '%s' "$out"
142
+ }
143
+ match_custom_domain() { # match_custom_domain <expected>:命中返回该域名,否则空
144
+ fc_get /2023-03-30/custom-domains | node -e '
145
+ const d = JSON.parse(require("fs").readFileSync(0, "utf8"));
146
+ const target = process.argv[1];
147
+ console.log((d.customDomains || []).some((c) => c.domainName === target) ? target : "");
148
+ ' "$1"
149
+ }
150
+
151
+ PROD_URL=""
152
+ ALLOW_CD=""
153
+ if [ -f "$DOMAIN_TMPL" ] && [ -z "${APP_DOMAIN_SUFFIX:-}" ]; then
154
+ # 单条 GetTrigger 而不是集合 ListTriggers:triggerName 由 persona 的 s.yaml.tmpl 契约固定
155
+ # 为 httpTrigger(写死是确定的,不是猜的)。
156
+ PROD_URL="$(fc_get "/2023-03-30/functions/${DEPLOY_NAME}/triggers/httpTrigger" | node -e '
157
+ const d = JSON.parse(require("fs").readFileSync(0, "utf8"));
158
+ console.log((d.httpTrigger && d.httpTrigger.urlInternet) || "");
159
+ ')"
160
+ MISS_HINT="未取到 FC 默认域名(函数 ${DEPLOY_NAME} 的 httpTrigger urlInternet 为空),检查 s deploy 是否真的建出了 httpTrigger"
161
+ # FC 默认域名(*.fcapp.run)对所有响应强制注入 Content-Disposition: attachment,没有
162
+ # 自定义域名就消不掉。使用者自带 key 履约的验收口径本来就是「公网可访问即可」
163
+ # (design ③:per-user 部署用 FC 默认域名,不要求品牌域名),所以降级成警告不判失败。
164
+ ALLOW_CD="--allow-content-disposition"
165
+ else
166
+ EXPECTED_DOM="${DEPLOY_NAME}.${APP_DOMAIN_SUFFIX:-app.clawos.chat}"
167
+ DOM="$(match_custom_domain "$EXPECTED_DOM")"
168
+ [ -n "$DOM" ] && PROD_URL="https://$DOM"
169
+ MISS_HINT="未取到自定义域名 $EXPECTED_DOM,检查 fc3-domain 是否成功 + DNS 泛解析 CNAME 是否生效"
170
+ fi
95
171
 
96
172
  echo ""
97
173
  echo "=================================================="
98
- if [ -n "$DOM" ]; then
99
- echo "::prod-url::https://$DOM"
100
- echo " ✅ 发布完成: https://$DOM"
174
+ if [ -n "$PROD_URL" ]; then
175
+ echo "::prod-url::$PROD_URL"
176
+ echo " ✅ 发布完成: $PROD_URL"
101
177
  echo "=================================================="
102
- bash "$KIT_DIR/scripts/verify.sh" "https://$DOM"
178
+ bash "$KIT_DIR/scripts/verify.sh" "$PROD_URL" $ALLOW_CD
103
179
  else
104
- echo "❌ 部署完成但未取到自定义域名 $EXPECTED_DOM,检查 fc3-domain 是否成功 + DNS *.app.clawos.chat CNAME 是否生效" >&2
180
+ echo "❌ 部署完成但$MISS_HINT" >&2
105
181
  exit 1
106
182
  fi
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import { execFileSync } from 'node:child_process'
3
- import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
3
+ import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync, readdirSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
  import { tmpdir } from 'node:os'
6
6
 
@@ -67,3 +67,140 @@ describe('publish.sh 占位符替换', () => {
67
67
  expect(r.out).toContain('secret: a|b&c\\d')
68
68
  })
69
69
  })
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // M1b Step 5c:账号级资源(自定义域名 + 证书)按履约切换
73
+ //
74
+ // 自定义域名与通配符证书都绑在**我们自己的**阿里云账号上,所以它只在「部署者供给阿里云 key」
75
+ // 这种履约下成立。使用者自带 key 时发布落在他自己的账号里,那里既没有我们的 DNS 也没有我们的
76
+ // 证书,硬渲染出来只会在 fc3-domain 那步失败——所以整段不渲染,用 FC 默认域名。
77
+ //
78
+ // 下面两组用例跑的是**真的 config.env**(不是手编样例):一组验它作为「可被 env 覆盖的默认值表」
79
+ // 的行为,一组验按同一个判据拼出来的 s.yaml 差异。
80
+ const DEFAULTS = join(__dirname, '..', '..', 'persona', 'defaults')
81
+ const KIT = join(DEFAULTS, 'persona-app-builder', 'extension-kit')
82
+
83
+ /** 复刻 publish.sh 的「source config.env → 打印结果」段。实现若改 publish.sh,本片段须同步。 */
84
+ function sourceConfigEnv(env: Record<string, string>): Record<string, string> {
85
+ const out = execFileSync(
86
+ 'bash',
87
+ ['-c', `set -euo pipefail; . "$1"; for v in APP_DOMAIN_SUFFIX CERT_ID REGION FC_CPU; do printf '%s=%s\\n' "$v" "\${!v}"; done`, 'cfg', join(KIT, 'config.env')],
88
+ { env: { ...process.env, ...env }, encoding: 'utf8' },
89
+ )
90
+ return Object.fromEntries(out.trim().split('\n').map((l) => {
91
+ const i = l.indexOf('=')
92
+ return [l.slice(0, i), l.slice(i + 1)]
93
+ }))
94
+ }
95
+
96
+ /**
97
+ * 复刻 publish.sh 的「按 kit 有没有 s.yaml.domain.tmpl + APP_DOMAIN_SUFFIX 拼模板 → 渲染」段。
98
+ * 同上,改脚本须同步。**判据的第一层是文件存在**——没有那个文件的 persona 压根不参与这套切换。
99
+ */
100
+ function renderSYaml(env: Record<string, string>): string {
101
+ const cfg = sourceConfigEnv(env)
102
+ const base = readFileSync(join(KIT, 'contract', 's.yaml.tmpl'), 'utf8')
103
+ const domTmpl = join(KIT, 'contract', 's.yaml.domain.tmpl')
104
+ const tmpl = existsSync(domTmpl) && cfg.APP_DOMAIN_SUFFIX
105
+ ? base + readFileSync(domTmpl, 'utf8')
106
+ : base
107
+ const r = render(tmpl, {
108
+ ...cfg,
109
+ ...env,
110
+ APP_NAME: 'demo', DEPLOY_NAME: 'demo-ab12', CODE_DIR: './server', FC_ENTRY: 'dist/main.js',
111
+ FC_RUNTIME: 'custom.debian12', NODE_LAYER: 'layer', FC_MEMORY: '512', FC_TIMEOUT: '60',
112
+ SUPABASE_URL: 'http://sb', SUPABASE_KEY: 'anon',
113
+ })
114
+ expect(r.err ?? '').toBe('')
115
+ return r.out!
116
+ }
117
+
118
+ describe('config.env 作为「可被 env 覆盖的默认值表」', () => {
119
+ it('env 里没有这几个变量 → 部署级默认(品牌域名 + 我们账号的 certId)', () => {
120
+ const clean = JSON.parse(
121
+ execFileSync('bash', ['-c',
122
+ `unset APP_DOMAIN_SUFFIX CERT_ID REGION; . "$1"; printf '{"d":"%s","c":"%s","r":"%s"}' "$APP_DOMAIN_SUFFIX" "$CERT_ID" "$REGION"`,
123
+ 'cfg', join(KIT, 'config.env')], { encoding: 'utf8' }),
124
+ )
125
+ expect(clean).toEqual({ d: 'app.clawos.chat', c: '25612343', r: 'cn-hangzhou' })
126
+ })
127
+
128
+ it('env 给了就以 env 为准(source 不覆盖上游注入的值)', () => {
129
+ const cfg = sourceConfigEnv({ APP_DOMAIN_SUFFIX: 'app.example.com', CERT_ID: '999', REGION: 'cn-beijing' })
130
+ expect(cfg.APP_DOMAIN_SUFFIX).toBe('app.example.com')
131
+ expect(cfg.CERT_ID).toBe('999')
132
+ expect(cfg.REGION).toBe('cn-beijing')
133
+ })
134
+
135
+ it('APP_DOMAIN_SUFFIX 显式空串保持为空(${VAR-d} 不是 ${VAR:-d})', () => {
136
+ // 这是「使用者自带 key」那条履约的开关:空串必须活下来,被默认值吃掉就等于开关失灵
137
+ expect(sourceConfigEnv({ APP_DOMAIN_SUFFIX: '' }).APP_DOMAIN_SUFFIX).toBe('')
138
+ // 对照:平台常量用 ${VAR:-d},空串没有意义,回落默认
139
+ expect(sourceConfigEnv({ FC_CPU: '' }).FC_CPU).toBe('0.35')
140
+ })
141
+ })
142
+
143
+ describe('两种履约渲染出来的 s.yaml 差异', () => {
144
+ it('部署级:带 fc3-domain 段 + 品牌域名 + certId', () => {
145
+ const out = renderSYaml({})
146
+ expect(out).toContain('component: fc3-domain')
147
+ expect(out).toContain('domainName: demo-ab12.app.clawos.chat')
148
+ expect(out).toContain('certId: 25612343')
149
+ })
150
+
151
+ it('使用者自带 key:整段不渲染,只剩函数 + httpTrigger(走 FC 默认域名)', () => {
152
+ const out = renderSYaml({ APP_DOMAIN_SUFFIX: '' })
153
+ expect(out).not.toContain('fc3-domain')
154
+ expect(out).not.toContain('certId')
155
+ expect(out).not.toContain('clawos.chat')
156
+ expect(out).toContain('triggerType: http')
157
+ })
158
+
159
+ it('换成使用者自己的域名与证书:两个值都跟着走', () => {
160
+ const out = renderSYaml({ APP_DOMAIN_SUFFIX: 'app.example.com', CERT_ID: '999' })
161
+ expect(out).toContain('domainName: demo-ab12.app.example.com')
162
+ expect(out).toContain('certId: 999')
163
+ })
164
+ })
165
+
166
+ // publish.sh 是 **persona 无关的共享脚本**,persona-dataclaw-builder 也走它。它有自己的一套
167
+ // extension-kit:functionName 不带 slug、自定义域名段写死在它自己的 s.yaml.tmpl 里
168
+ // (`domainName: auto`)、没有 s.yaml.domain.tmpl。5c 的判据必须是「有没有那个文件」而不是
169
+ // 「APP_DOMAIN_SUFFIX 空不空」——只看后者的话,dataclaw 会被当成「使用者自带 key」拖进
170
+ // FC 默认域名那条分支,而它的函数名规则根本对不上,等于给一个没参与的 persona 换了发布路径。
171
+ describe('未参与 5c 的 persona(dataclaw-builder)不受影响', () => {
172
+ const DC_KIT = join(DEFAULTS, 'persona-dataclaw-builder', 'extension-kit')
173
+
174
+ it('没有 s.yaml.domain.tmpl —— 判据的第一层就把它排除在外', () => {
175
+ expect(existsSync(join(DC_KIT, 'contract', 's.yaml.domain.tmpl'))).toBe(false)
176
+ })
177
+
178
+ it('它的自定义域名段仍在自己的 s.yaml.tmpl 里,不依赖 APP_DOMAIN_SUFFIX', () => {
179
+ const text = readFileSync(join(DC_KIT, 'contract', 's.yaml.tmpl'), 'utf8')
180
+ expect(text).toContain('fc3-domain')
181
+ expect(text).not.toContain('__APP_DOMAIN_SUFFIX__')
182
+ })
183
+
184
+ it('它的 config.env 里没有 APP_DOMAIN_SUFFIX(所以那个变量对它恒空)', () => {
185
+ expect(readFileSync(join(DC_KIT, 'config.env'), 'utf8')).not.toContain('APP_DOMAIN_SUFFIX')
186
+ })
187
+ })
188
+
189
+ // deploy-kit 的脚本 macOS(BSD userland)和 Linux(GNU coreutils,= M1b 的 Pod)都要跑,
190
+ // 两套 userland 语义有分歧的写法就是定时炸弹:`mktemp -t prefix` BSD 自己补随机后缀,
191
+ // GNU 要求模板以 X 结尾,后者直接 `too few X's in template` 退 1(coreutils 9.1 实测),
192
+ // `set -e` 下当场打死脚本 —— 这条在 2026-09-03 的复审里真炸过一次。
193
+ // 本机跑测试只覆盖得到 BSD 那半,所以用静态形态守:写全 `XXXXXX` 模板两边都过。
194
+ // 只看非注释行 —— 注释里为了讲清楚这个坑就得写出那个形态。
195
+ describe('deploy-kit 脚本的 BSD / GNU 可移植性', () => {
196
+ it('不用 `mktemp -t`(GNU 要求模板带 X)', () => {
197
+ const hits: string[] = []
198
+ for (const f of readdirSync(__dirname).filter((n) => n.endsWith('.sh'))) {
199
+ for (const line of readFileSync(join(__dirname, f), 'utf8').split('\n')) {
200
+ if (line.trim().startsWith('#')) continue
201
+ if (/\bmktemp\s+-t\b/.test(line)) hits.push(`${f}: ${line.trim()}`)
202
+ }
203
+ }
204
+ expect(hits).toEqual([])
205
+ })
206
+ })
@@ -1,15 +1,21 @@
1
1
  #!/usr/bin/env bash
2
2
  # 发布后健康检查:可达性 + Content-Type + 【是否被强制下载】
3
- # 用法: verify.sh <url>
3
+ # 用法: verify.sh <url> [--allow-content-disposition]
4
4
  #
5
5
  # 失败语义(spec 2026-06-03-app-builder-publish-scaffold-design follow-up):
6
- # 首页非 200 / 检测到 Content-Disposition 强制下载 → exit 1。
6
+ # 首页非 200 → exit 1。
7
+ # 检测到 Content-Disposition 强制下载 → 默认 exit 1;带 --allow-content-disposition 时
8
+ # 只警告不失败 —— FC 默认域名(*.fcapp.run)对所有响应强制注入这个头,没有自定义域名就
9
+ # 消不掉,而「使用者自带 key」履约的验收口径本来就只到「公网可访问」(design ③)。
10
+ # 调用方只在那条路径上传这个 flag,别当通用开关用。
7
11
  # publish.sh 不加 `|| true` 兜底,verify 失败会让 publish.sh 整体 exit 非 0,
8
12
  # 触发 daemon runner 的 publish-failed 路径:广播帧 + 回灌 chat user_text 让 assistant 接管修复。
9
13
  #
10
14
  # 信息原则:失败原因 echo 到 stderr(runner 的 errorSummary 抠 stderr 最后 20 行)。
11
15
  set -euo pipefail
12
- URL="${1:?用法: verify.sh <url>}"
16
+ URL="${1:?用法: verify.sh <url> [--allow-content-disposition]}"
17
+ ALLOW_CD=0
18
+ [ "${2:-}" = "--allow-content-disposition" ] && ALLOW_CD=1
13
19
 
14
20
  echo "== 健康检查: $URL =="
15
21
  code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "$URL/" || echo 000)
@@ -21,12 +27,17 @@ hdr=$(curl -s -D - -o /dev/null --max-time 30 "$URL/" || true)
21
27
  has_cd=0
22
28
  if echo "$hdr" | grep -qi 'content-disposition'; then
23
29
  has_cd=1
24
- echo "❌ 检测到 Content-Disposition —— 浏览器会下载而非渲染!确认走的是 fc3-domain 自定义域名,而不是 fcapp.run" >&2
30
+ if [ "$ALLOW_CD" = "1" ]; then
31
+ echo "⚠️ 检测到 Content-Disposition —— 浏览器会下载而非渲染。这是 FC 默认域名(*.fcapp.run)的平台行为,"
32
+ echo " 使用者自带 key 履约下没有自定义域名可绑,按验收口径只警告不判失败。要浏览器直出得绑自定义域名。"
33
+ else
34
+ echo "❌ 检测到 Content-Disposition —— 浏览器会下载而非渲染!确认走的是 fc3-domain 自定义域名,而不是 fcapp.run" >&2
35
+ fi
25
36
  else
26
37
  echo "✓ 无 Content-Disposition,浏览器可正常渲染网页"
27
38
  fi
28
39
 
29
- if [ "$code" = "200" ] && [ "$has_cd" = "0" ]; then
40
+ if [ "$code" = "200" ] && { [ "$has_cd" = "0" ] || [ "$ALLOW_CD" = "1" ]; }; then
30
41
  echo "✓ 全链路 OK"
31
42
  exit 0
32
43
  fi
@@ -1,18 +1,15 @@
1
1
  {
2
- "_comment": "preinstall ship 进 daemon defaults,daemon 启动时 refreshDaemonManagedDirs 把这文件同步到 ~/.clawd*/personas/persona-app-builder/.mcp.json。cc CLI 启动时(cwd=projects/<name>/)会向上找 persona dir 的 .mcp.json 自动加载。用 @aliyun-rds/supabase-mcp-server Mode 2 单实例直连(不再用 aliyun AK),底下接阿里云托管 supabase(公网 endpoint 121.196.249.178:80,项目 ra-ae0i57kef06s6ix / clawd-app-builder,杭州可用区J)。**不要往这里加 --jwt-secret**:jwt-secret 是该实例的 JWT 签发密钥,拿到它可以自签任意 role/任意 user 的 token(含 service_role),下发出去等于把 anon/service key 的轮换能力一起交出去;而 MCP server 里它唯一的用处是 verify_jwt_secret 这个自检工具报个 found/preview(--jwt-secret 在其 CLI 里就标了 optional),建表读写和 auth user 管理都不经过它。**这份文件随 OTA 发给所有用户,所以这里放的每一把 key 都等于公开**:该实例上除 demo 数据外还有 clawd 自己的 clawd_prod/clawd_int schema(device_bindings 等),收敛方案见 doc/glossary/persona-cloud.md。",
2
+ "_comment": "preinstall ship 进 daemon defaults,daemon 启动时 refreshDaemonManagedDirs 把这文件同步到 ~/.clawd*/personas/persona-app-builder/.mcp.json。用 @aliyun-rds/supabase-mcp-server Mode 2 单实例直连(不再用 aliyun AK),底下接阿里云托管 supabase。**这里只有声明,没有值**:url / anon / service-role 三个值由 daemon 在 spawn 时从凭据表渲进 per-server env(见 daemon/src/persona/mcp-secret-render.ts),渲染产物是另一份 0600 文件、同名 server 压过本文件,所以 agent 在 persona 目录里读不到任何 key。这三个 env 名不是我们发明的——@aliyun-rds/supabase-mcp-server 的 commander option 默认值就取 process.env.SUPABASE_URL / SUPABASE_ANON_KEY / SUPABASE_SERVICE_ROLE_KEY,所以不再传 --supabase-* CLI 参数(参数会进 ps 输出,等于没藏)。**不要往这里加 --jwt-secret / SUPABASE_AUTH_JWT_SECRET**:jwt-secret 是该实例的 JWT 签发密钥,拿到它可以自签任意 role/任意 user 的 token(含 service_role),下发出去等于把 anon/service key 的轮换能力一起交出去;而 MCP server 里它唯一的用处是 verify_jwt_secret 这个自检工具报个 found/preview。值本身随 OTA 下发的边界与收敛方向见 doc/glossary/persona-cloud.md。",
3
3
  "mcpServers": {
4
4
  "supabase": {
5
5
  "type": "stdio",
6
6
  "command": "npx",
7
- "args": [
8
- "@aliyun-rds/supabase-mcp-server",
9
- "--supabase-url",
10
- "http://121.196.249.178:80",
11
- "--supabase-anon-key",
12
- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNzgzMzEwODU1LCJleHAiOjEzMjkzOTUwODU1fQ.sHLZH8q2SGWYbNPEFOzTFlQEtjVK3z1DlEqoU_5Uyyg",
13
- "--supabase-service-role-key",
14
- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJzZXJ2aWNlX3JvbGUiLCJpYXQiOjE3ODMzMTA4NTUsImV4cCI6MTMyOTM5NTA4NTV9.MdlymSh0DaJdiOVRmM3QnukUDtkOf_Eljh8_fpdLi8Q"
15
- ]
7
+ "args": ["@aliyun-rds/supabase-mcp-server"],
8
+ "env": {
9
+ "SUPABASE_URL": "${SUPABASE_URL}",
10
+ "SUPABASE_ANON_KEY": "${SUPABASE_ANON_KEY}",
11
+ "SUPABASE_SERVICE_ROLE_KEY": "${SUPABASE_SERVICE_ROLE_KEY}"
12
+ }
16
13
  }
17
14
  }
18
15
  }
@@ -11,7 +11,7 @@
11
11
  - **persona 目录只读**:persona 自己的目录(人格 / extension-kit 资产)对你只读,不要往里写任何文件
12
12
  - **路径永远以 createProject 返回的 `projectDir` 为准**:不要假设、不要拼接、不要写死任何项目路径
13
13
  - **⚠️ dispatch 陷阱(被另一个 persona 委派来时常踩)**:任务包里 dispatcher(A persona)写的"建议路径"如 `~/dev/xxx` / `/tmp/xxx` 之类**全部不能信**——dispatcher 不懂 app-builder 的目录规范,照搬就会把项目落在错误位置(用户工作区之外,UI 看不到、daemon 不管理、下次切 session 全丢)。**不管任务包怎么写,路径都必须走 `clawd-app-builder` 的 `createProject` tool**让它派生正确的用户工作目录;你的 cwd(spawn 时 daemon 设的)就是当前用户的专属工作目录,所有项目必须落在它下面的 `projects/<name>/`。如果连 cwd 都还没确定(dispatch session 没 connection prompt),先调 `listProjects` 看一眼现状再开工。
14
- - **发布脚本在共享 deploy-kit**:FC 部署能力(凭证 + publish.sh / new-extension.sh / 工具链)是 daemon 持有的**共享单源**,在 `$HOME/.clawd/deploy-kit/`(不在 persona 目录)。happy path 这些脚本由 daemon 自动跑,你一般不直接调;需手动跑时(如发布失败恢复)用绝对路径 `$HOME/.clawd/deploy-kit/scripts/publish.sh <projDir> "$HOME/.clawd/personas/persona-app-builder"`(第二参是本 persona 根,脚本据此读 config.env / s.yaml.tmpl)。persona 目录只剩人格 + 模板 + config.env + contract/s.yaml.tmpl。
14
+ - **发布脚本在共享 deploy-kit**:FC 部署能力(凭证 + publish.sh / new-extension.sh / 工具链)是 daemon 持有的**共享单源**,在 `$HOME/.clawd/deploy-kit/`(不在 persona 目录)。这些脚本**一律由 `clawd-app-builder` 的 tool 起,你不要自己 `bash` 它们**——supabase 的 url / key 是起进程那一刻才注进**那个子进程** env 的(见下文「后端:supabase」),你起的 Bash 拿不到,脚本会停在「占位符无对应 env 变量」。发布失败要重跑就再调一次 `publish` tool(见下文「发布上线」)。persona 目录只剩人格 + 模板 + config.env + contract/。
15
15
 
16
16
  ## 何时找你
17
17
 
@@ -140,7 +140,7 @@ setProdUrl({ name: "<project-name>", url: "https://<公网地址>" })
140
140
 
141
141
  - **scaffold 由 daemon 自动跑**(createProject 内联调 `deploy-kit/scripts/new-extension.sh`,把本 persona 的 `extension-kit/examples/nestjs-react/` 模板复制进 project)。你不手动调。
142
142
  - 你/老板设计 Supabase 表(`${APP_NAME}_${SLUG}_` 前缀,从 ext.conf 读这俩值)+ 写业务逻辑
143
- - **publish 由「发布上线」按钮触发 daemon 自动跑** `deploy-kit/scripts/publish.sh <projDir> <persona根>`(build + 部署 FC + 绑域名 + 验证)。仅发布失败时你接管手动重跑(见下文「发布上线」)。
143
+ - **publish 由「发布上线」按钮触发 daemon 自动跑** `deploy-kit/scripts/publish.sh <projDir> <persona根>`(build + 部署 FC + 绑域名 + 验证)。仅发布失败时你接管,改完问题**再调一次 `publish` tool**(见下文「发布上线」)。
144
144
 
145
145
  设计是「契约层(框架/语言无关)+ 可插拔技术栈示例」:换框架只改 `ext.conf`,换语言改运行时层 + `contract/bootstrap`。**踩过的 FC 坑全固化在配方记忆 `fc-nodejs-deploy-recipe` 和 `contract/` 里,开工前先看。**
146
146
 
@@ -151,7 +151,7 @@ setProdUrl({ name: "<project-name>", url: "https://<公网地址>" })
151
151
  - **全局唯一身份**:scaffold 时 `new-extension.sh` 用 `openssl rand -hex 2` 生成 4 位 `SLUG` 并连同 `APP_NAME` 一起 bake 进 `ext.conf`,**锁定后稳定**。多用户/多副本同名 app 都不撞 FC 函数 / 域名 / 共享 Supabase 表。两种派生形式:
152
152
  - **DNS-safe**(连字符):`DEPLOY_NAME=${APP_NAME}-${SLUG}` → FC functionName / 子域名 / `s` 项目名
153
153
  - **SQL-safe**(下划线):`${APP_NAME}_${SLUG}_` → Supabase 表前缀(见「后端:supabase」)
154
- - **自定义域名 = `<DEPLOY_NAME>.app.clawos.chat`(HTTPS 直出)**。根域 `clawos.chat` 一条泛解析 CNAME `*.app → <FC accountId>.cn-hangzhou.fc.aliyuncs.com` 一劳永逸;TLS 走通配符证书 `*.app.clawos.chat`(已托管到 FC 账号 CAS,`certId` 在 `extension-kit/config.env`,3 个月自动续期,证书 ID 不变)。**不要**再回退 `domainName: auto` —— 那派的 `xxx.fcapp.run` 阿里云 3 天回收。
154
+ - **自定义域名 = `<DEPLOY_NAME>.$APP_DOMAIN_SUFFIX`(默认 `app.clawos.chat`,HTTPS 直出)**。根域 `clawos.chat` 一条泛解析 CNAME `*.app → <FC accountId>.cn-hangzhou.fc.aliyuncs.com` 一劳永逸;TLS 走通配符证书 `*.app.clawos.chat`(已托管到 FC 账号 CAS,`certId` 默认值在 `extension-kit/config.env`,3 个月自动续期,证书 ID 不变)。**这一段只在用 clawd 自己那把阿里云 key 时成立**:域名和证书都绑在那个账号上,换成使用者自带的 key 时 `APP_DOMAIN_SUFFIX` 置空、整段 `fc3-domain` 不渲染,产物走 FC 默认域名(`publish.sh` 两条分支同一个判据)。**不要**手动回退 `domainName: auto` —— 那派的 `xxx.fcapp.run` 阿里云 3 天回收。
155
155
  - 工具链:**`aliyun` CLI + Serverless Devs(`s`)**——即 `publish.sh` 内部做的事。两者由 `deploy-kit/scripts/ensure-toolchain.sh` 在 publish/remove 动手前自动检测+安装(缺 `aliyun`:有 brew 走 `brew install aliyun-cli`,否则下官方二进制到 `~/.clawd/bin`;缺 `s`:`npm i -g @serverless-devs/s`),新机器无需手动装。接管发布失败时**不必再排查"CLI 没装"**。
156
156
  - 实时推送用 Supabase Realtime(FC 不维持长连接)。
157
157
 
@@ -160,10 +160,10 @@ setProdUrl({ name: "<project-name>", url: "https://<公网地址>" })
160
160
  **`supabase`** —— 后端数据/认证/存储,唯一的 MCP 支柱。这份 Supabase **是阿里云托管 supabase 实例**(`http://121.196.249.178:80`),全局 MCP / clawos / lovagent / moltoffer / clawd 都指向这一台。**不是**云上 `supabase.com` 的实例。开工前确认能连上:
161
161
 
162
162
  - 读表结构、跑 SQL、看 auth 用户都走这个 MCP,不要凭记忆猜 schema
163
- - 模板里 `extension-kit/config.env` + `server/.env.example` 已对齐这台阿里云托管实例(SUPABASE_URL / SUPABASE_KEY),创建 project 时拷进项目目录的 `.env` 也是同一套
163
+ - **凭据值你看不到,也不需要看到**:`extension-kit/config.env` 与 `server/.env.example` 里只有变量名(`${SUPABASE_URL}` / `${SUPABASE_KEY}`),值由 daemon 在起 MCP server / 起 publish 脚本时才注进那个子进程。创建 project 时 scaffold 会把项目的 `server/.env` 渲好,直接 `pnpm dev` 就能连上——**不要**去别处找 key 往文件里抄
164
164
  - **红线**:这是共享生产库。建表 / 迁移前先 `list_tables` 看清现状,新项目的表**必须**用 `${APP_NAME}_${SLUG}_` 前缀沉到 `public` schema(如 `helloworld_a1b2_click_counter`,`APP_NAME` 和 `SLUG` 都从项目 `ext.conf` 读,scaffold 时 bake,**不要自己另起一套 slug**),避免和 clawos / 其它项目 / 别人同名 app 的表撞名;**绝不** drop / alter clawos 已有的表,不确定哪些是 clawos 的就先问老板
165
165
 
166
- **易踩的坑**:`config.env` 的 `SUPABASE_URL` 是**唯一真源** —— MCP 工具连它,`publish.sh` 也把它渲染进 s.yaml 给线上用。任何一处(项目 `.env`、`.env.example`)写成别的地址,就会**MCP 建表落一台 / server 通过 supabase-js 查另一台**,两边 PostgREST 的 schema cache 各自独立 → `PGRST205 schema cache 找不到表`。看到 PGRST 系列错码,先拿项目里的 `SUPABASE_URL` 跟 `config.env` 逐字比对,一致了再怀疑 schema cache。
166
+ **易踩的坑**:唯一真源是 **daemon 的凭据表**(`~/.clawd/secrets/` 覆盖 clawd 自带的那份)—— supabase MCP 连它,`publish.sh` 也把它渲染进 s.yaml 给线上用,scaffold 渲项目 `.env` 还是它。项目 `.env` 被手改成别的地址,就会**MCP 建表落一台 / server 通过 supabase-js 查另一台**,两边 PostgREST 的 schema cache 各自独立 → `PGRST205 schema cache 找不到表`。看到 PGRST 系列错码,先看项目 `.env` 的 `SUPABASE_URL` 是不是被人动过;没动过再怀疑 schema cache。
167
167
 
168
168
  ## 阿里云凭证:在共享 deploy-kit 的 `.secrets/`,分层 override
169
169
 
@@ -199,7 +199,7 @@ source "$KIT_DIR/scripts/ensure-credentials.sh" && ensure_credentials
199
199
  1. **创建 project**(fresh session 进来时)—— 问老板想做啥 app + 反问名字 + 调 `createProject` tool(详见上文「触发 createProject」)。已绑 session 跳过这一步
200
200
  2. **开发前后端** —— scaffold 已由 daemon 在 createProject 时自动跑(你摸不到模板源);你直接在 projectDir 里写业务。复杂任务走 superpowers 节奏,简单任务 TodoWrite 跟踪(分级见下)
201
201
  3. **接 Supabase** —— 数据/认证/存储走 supabase MCP,schema 以线上为准,新表加 `${APP_NAME}_${SLUG}_` 前缀(从 ext.conf 读)
202
- 4. **部署** —— 老板点「发布上线」按钮,daemon 自动跑 `deploy-kit/scripts/publish.sh`(build + 发 FC + 绑域名 + 验证 + 写 prodUrl),happy path 跳过你。仅发布失败时你接管手动重跑(见下文「发布上线」)。
202
+ 4. **部署** —— 老板点「发布上线」按钮,daemon 自动跑 `deploy-kit/scripts/publish.sh`(build + 发 FC + 绑域名 + 验证 + 写 prodUrl),happy path 跳过你。仅发布失败时你接管,改完问题**重调那个 RPC**(见下文「发布上线」)。
203
203
 
204
204
  ## build 模式(clawd 双栏实时预览)
205
205
 
@@ -249,7 +249,7 @@ await app.listen(Number(process.env.CLAWD_PREVIEW_PORT));
249
249
 
250
250
  **告别 marker**:dev / prod 预览都不再需要你输出 `<builder-preview ...>` 标记(单栏 refactor 2026-06-02 §5.6.1 砍掉了所有 marker 路径)。
251
251
  - **dev 预览**:UI 通过 session ↔ project ↔ port 数据流自动加载,daemon state 驱动
252
- - **prod 预览**:跑完 `publish.sh` 后调 `setProdUrl` tool(见上文),写入 `.clawd-project.json.prodUrl` 后 UI 自动切线上 URL
252
+ - **prod 预览**:`publish` tool 成功后流水线自己写 `.clawd-project.json.prodUrl`,UI 自动切线上 URL,你不用调 `setProdUrl`(那个 tool 留给「URL 是你从别处拿到的」这类边角情况)
253
253
 
254
254
  ### 发布上线(脚手架化 2026-06-03)
255
255
 
@@ -259,10 +259,17 @@ await app.listen(Number(process.env.CLAWD_PREVIEW_PORT));
259
259
 
260
260
  1. 读项目目录下的 `.publish.log` 完整日志
261
261
  2. 定位并修复问题(改源码 / 改 `ext.conf` / 检查 `.secrets/aliyun.env` 等)
262
- 3. 重跑 `bash "$HOME/.clawd/deploy-kit/scripts/publish.sh" <projDir> "$HOME/.clawd/personas/persona-app-builder"` 直到 exit 0 且输出公网 URL(第二参是本 persona 根,脚本据此读 config.env / s.yaml.tmpl)
263
- 4. 调 `setProdUrl` tool 把 URL 写入项目元数据(同上文)
262
+ 3. **再调一次 `publish` tool** 重跑,直到它给出公网 URL。成功路径由流水线自己写 prodUrl,
263
+ 你不用再调 `setProdUrl`
264
264
 
265
- **注意**:重跑期间不要自己调 `publish` tool(同一条流水线已被老板那次触发占着,会被当作"已 in-flight"拒绝),手动跑脚本即可。失败回灌的消息开头固定是 `发布失败:[`,识别此前缀即触发接管流程。
265
+ **重跑必须再调 tool,不要自己 `bash publish.sh`。** 这不是风格问题:supabase 的 url / key 是
266
+ 起 publish.sh 那一刻才注进**那个子进程**的 env(见 `doc/glossary/persona-cloud.md` 的「凭据表」),
267
+ 你自己起的 Bash 拿不到,脚本会停在 `❌ s.yaml.tmpl 占位符 __SUPABASE_URL__ 无对应 env 变量`。
268
+ **看到那句报错时不要去找 key 手动 export、也不要往 `config.env` 里写值** —— 那正好把凭据又摊回
269
+ 所有用户的机器上;正确动作就是改调 tool。
270
+
271
+ 失败之后 in-flight 记录已经被清掉了,所以 `publish` tool 会正常受理(`already-publishing`
272
+ 只在**真的还在跑**的时候返回)。失败回灌的消息开头固定是 `发布失败:[`,识别此前缀即触发接管流程。
266
273
 
267
274
  **dev server 起不来怎么排查**:daemon 把 supervisor 输出(`dev-server.start` / `.stdout` / `.stderr` / `.spawn-failed` / `.exit`)打进 `~/.clawd/clawd.log`,老板可以 `grep dev-server ~/.clawd/clawd.log` 看真凶。常见原因:
268
275
  - 端口 6XXX 被系统其他进程占了 → 右栏 header 齿轮菜单 → "Update Port..." 换段内另一个
@@ -1,30 +1,36 @@
1
1
  # ===== 平台级固定变量(一次配好,所有 extension 共用)=====
2
- # 阿里云凭证不在这里:从 .secrets/aliyun.env 现读(见 CLAUDE.md)
2
+ #
3
+ # **这里没有任何凭据值。** supabase 的 url / key 由 daemon 在 spawn publish.sh /
4
+ # new-extension.sh 时从凭据表注进那个子进程的 env(见 daemon/src/deploy/secret-table.ts),
5
+ # 本文件随 OTA 发给所有用户、agent 一个 Read 就看得见,不是放 key 的地方。
6
+ #
7
+ # 每个变量都写成 ${VAR-default} / ${VAR:-default} 形式:publish.sh 是 `source` 本文件的,
8
+ # 直接写 `X=值` 会把上游注进来的 env **覆盖掉**,参数化就白做了。
9
+ # ${VAR:-d} = 未设或为空 → 用 d (平台常量,空值没有意义)
10
+ # ${VAR-d} = 只有未设 → 用 d (显式设成空串是一种有意义的履约,见下面域名段)
3
11
 
4
- REGION=cn-hangzhou
12
+ REGION="${REGION:-cn-hangzhou}"
5
13
 
6
14
  # FC 运行时:custom.debian12(纯净) + 官方 Node22 层(自带 node + 原生 WebSocket)
7
15
  # 换语言时改这两行 + 换 contract/bootstrap(见 README「换框架/语言」)
8
- FC_RUNTIME=custom.debian12
9
- NODE_LAYER=acs:fc:cn-hangzhou:official:layers/Nodejs22/versions/1
10
-
11
- # Supabase(阿里云托管 supabase 实例!建表务必加 <app>_ 前缀沉到 public schema,避免撞名)
12
- # 公网 endpoint 121.196.249.178:80(项目 ra-ae0i57kef06s6ix / clawd-app-builder,杭州可用区J)
13
- # 全局 MCP / clawos / lovagent / moltoffer / clawd 都指向这一台。
14
- # 下面两行是**唯一真源**:examples/*/server/.env.example 必须跟这里逐值一致
15
- # (defaults-supabase-parity.spec.ts 守这条)。不一致 = 本地连一台、线上连另一台,
16
- # 两边 PostgREST 的 schema cache 各自独立 → PGRST205 找不到表。
17
- SUPABASE_URL=http://121.196.249.178:80
18
- # anon/publishable key(公开可暴露,客户端用)。阿里云托管实例的 anon key,跟 supabase MCP 用同一把。
19
- SUPABASE_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNzgzMzEwODU1LCJleHAiOjEzMjkzOTUwODU1fQ.sHLZH8q2SGWYbNPEFOzTFlQEtjVK3z1DlEqoU_5Uyyg
16
+ FC_RUNTIME="${FC_RUNTIME:-custom.debian12}"
17
+ NODE_LAYER="${NODE_LAYER:-acs:fc:cn-hangzhou:official:layers/Nodejs22/versions/1}"
20
18
 
21
19
  # FC 资源规格(够跑轻量 extension;按需改)
22
- FC_CPU=0.35
23
- FC_MEMORY=512
24
- FC_TIMEOUT=60
20
+ FC_CPU="${FC_CPU:-0.35}"
21
+ FC_MEMORY="${FC_MEMORY:-512}"
22
+ FC_TIMEOUT="${FC_TIMEOUT:-60}"
25
23
 
26
- # 自定义域名 SSL 证书:已上传到 FC 账号 CAS(数字证书管理服务),s.yaml 走 certId 引用
27
- # 证书域名 *.app.clawos.chat(通配符),覆盖所有 <APP_NAME>.app.clawos.chat
28
- # 续期:阿里云"自动托管"开着,3 个月自动续,certId 不变
29
- # 更新证书:aliyun cas UploadUserCertificate 上传后改这里
30
- CERT_ID=25612343
24
+ # ===== 账号级资源:按履约切换 =====
25
+ # 自定义域名与它的证书都绑在**我们自己的**阿里云账号上:根域 clawos.chat 一条泛解析 CNAME
26
+ # `*.app` → FC,通配符证书 *.app.clawos.chat 托管在该账号的 CAS(3 个月自动续,certId 不变)。
27
+ # 所以它只在「部署者供给阿里云 key」这种履约下成立。
28
+ #
29
+ # 部署级履约(默认) : APP_DOMAIN_SUFFIX=app.clawos.chat → 渲染自定义域名段,产物 URL 带品牌域名
30
+ # 使用者自带 key 的履约 : APP_DOMAIN_SUFFIX=""(显式空串) → 不渲染自定义域名段,用 FC 默认域名
31
+ #
32
+ # 后者是必须的:发布落在使用者自己的 FC 账号里,那个账号既没有我们的 DNS 也没有我们的证书,
33
+ # 硬渲染出来只会在 fc3-domain 那步失败。公网可访问即可,品牌域名不进 per-user 履约的验收口径。
34
+ APP_DOMAIN_SUFFIX="${APP_DOMAIN_SUFFIX-app.clawos.chat}"
35
+ # 更新证书:aliyun cas UploadUserCertificate 上传后覆盖这个默认值(或用 env 覆盖)。
36
+ CERT_ID="${CERT_ID:-25612343}"
@@ -0,0 +1,19 @@
1
+
2
+ # 自定义域名段 —— **只在部署级履约下渲染**(APP_DOMAIN_SUFFIX 非空)。
3
+ # publish.sh 把本文件接在 s.yaml.tmpl 后面再跑同一套占位符替换,所以这里的缩进
4
+ # 必须跟 s.yaml.tmpl 的 resources: 子项对齐。
5
+ # 绑 __DEPLOY_NAME__.__APP_DOMAIN_SUFFIX__(根域已加泛解析 CNAME 到 FC);
6
+ # 证书走 FC 账号 CAS 托管(certId 见 config.env),续期 FC 自动跟新。
7
+ __DEPLOY_NAME__Domain:
8
+ component: fc3-domain
9
+ props:
10
+ region: ${vars.region}
11
+ domainName: __DEPLOY_NAME__.__APP_DOMAIN_SUFFIX__
12
+ protocol: HTTP,HTTPS
13
+ certConfig:
14
+ certId: __CERT_ID__
15
+ routeConfig:
16
+ routes:
17
+ - path: /*
18
+ functionName: __DEPLOY_NAME__
19
+ qualifier: LATEST
@@ -5,6 +5,10 @@
5
5
  # __APP_NAME__ = 用户给的项目名(描述/可读用)
6
6
  # __DEPLOY_NAME__ = APP_NAME-<4位 slug>,FC functionName / 子域名 / s 项目名/资源 key 走这个
7
7
  # 保证多用户/多副本同名时不撞 FC 函数 + 不撞域名
8
+ #
9
+ # **自定义域名段不在本文件里**,在同目录的 s.yaml.domain.tmpl。publish.sh 只在
10
+ # APP_DOMAIN_SUFFIX 非空时把那一段接到本文件后面再渲染 —— 使用者自带阿里云 key 时
11
+ # 那个账号没有我们的 DNS 与证书,渲染出来必挂(见 config.env 的「账号级资源:按履约切换」)。
8
12
  edition: 3.0.0
9
13
  name: __DEPLOY_NAME__-app
10
14
  # clawd 自己的 Serverless Devs profile,写在 deploy-kit 私有 s-home 里(见 scripts/ensure-credentials.sh)。
@@ -46,19 +50,3 @@ resources:
46
50
  triggerConfig:
47
51
  authType: anonymous
48
52
  methods: [GET, POST, PUT, DELETE, HEAD]
49
-
50
- # 自定义域名:绑 *.app.clawos.chat 子域名(根域 DNS 已加泛解析 CNAME 到 FC)
51
- # 证书走 FC 账号 CAS 托管(certId 在 config.env 里),续期 FC 自动跟新
52
- __DEPLOY_NAME__Domain:
53
- component: fc3-domain
54
- props:
55
- region: ${vars.region}
56
- domainName: __DEPLOY_NAME__.app.clawos.chat
57
- protocol: HTTP,HTTPS
58
- certConfig:
59
- certId: __CERT_ID__
60
- routeConfig:
61
- routes:
62
- - path: /*
63
- functionName: __DEPLOY_NAME__
64
- qualifier: LATEST
@@ -1,7 +1,8 @@
1
- # Supabase 连接串。**唯一真源是 extension-kit/config.env**——publish.sh 从那里读值渲染进
2
- # s.yaml 的 environmentVariables,线上跑的就是那一份。这里是本地 dev 用的副本(copy 成 .env),
3
- # 改实例地址时两处要一起改,否则本地连一台、线上连另一台,两边 PostgREST schema cache 各自独立,
4
- # 症状是 PGRST205 找不到表。(defaults-supabase-parity.spec.ts 守这条一致性。)
5
- SUPABASE_URL=http://121.196.249.178:80
6
- SUPABASE_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNzgzMzEwODU1LCJleHAiOjEzMjkzOTUwODU1fQ.sHLZH8q2SGWYbNPEFOzTFlQEtjVK3z1DlEqoU_5Uyyg
1
+ # supabase 连接串的**唯一真源是 daemon 的凭据表**(`~/.clawd/secrets/` 覆盖 clawd 自带那份)。
2
+ # 本文件是模板:scaffold(new-extension.sh)把下面的占位引用展开成同目录的 `.env` 给本地 dev 用;
3
+ # 线上那份由 publish.sh 渲进 s.yaml 的 environmentVariables。两条路取同一张表的同一个变量名,
4
+ # 所以不会出现「本地连一台、线上连另一台」的漂移(那会表现成 PGRST205 找不到表)。
5
+ # 要换实例改凭据表,别改这里的值——改了下次 scaffold 就被覆盖回去。
6
+ SUPABASE_URL=${SUPABASE_URL}
7
+ SUPABASE_KEY=${SUPABASE_KEY}
7
8
  PORT=3000