@clawos-dev/clawd 0.2.546 → 0.2.548

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.
Files changed (33) hide show
  1. package/dist/app-builder-plugin/mcp-server.cjs +35 -35
  2. package/dist/cli.cjs +518 -663
  3. package/dist/persona-defaults/persona-app-builder/.mcp.json +17 -2
  4. package/dist/persona-defaults/persona-app-builder/CLAUDE.md +8 -9
  5. package/dist/persona-defaults/persona-app-builder/extension-kit/README.md +6 -7
  6. package/dist/persona-defaults/persona-app-builder/extension-kit/contract/s.yaml.tmpl +3 -2
  7. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +96 -0
  8. package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/publish.sh +3 -3
  9. package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/remove-extension.sh +2 -2
  10. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/mcp-server.cjs +25397 -0
  11. package/dist/persona-defaults/persona-dataclaw-builder/.mcp.json +14 -1
  12. package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +1 -1
  13. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/README.md +6 -7
  14. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/config.env +1 -1
  15. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/contract/s.yaml.tmpl +3 -2
  16. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/contract/bootstrap +22 -0
  17. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +96 -0
  18. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-toolchain.sh +65 -0
  19. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/new-extension.sh +71 -0
  20. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/publish.sh +205 -0
  21. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/remove-extension.sh +89 -0
  22. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/verify.sh +49 -0
  23. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/mcp-server.cjs +25397 -0
  24. package/package.json +1 -1
  25. package/dist/deploy-kit/.secrets/aliyun.env.example +0 -7
  26. package/dist/deploy-kit/.secrets/aliyun.env.local.example +0 -12
  27. package/dist/deploy-kit/scripts/ensure-credentials.sh +0 -111
  28. package/dist/deploy-kit/scripts/ensure-credentials.spec.ts +0 -438
  29. package/dist/deploy-kit/scripts/publish.spec.ts +0 -278
  30. /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/contract/bootstrap +0 -0
  31. /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/ensure-toolchain.sh +0 -0
  32. /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/new-extension.sh +0 -0
  33. /package/dist/{deploy-kit → persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit}/scripts/verify.sh +0 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "数据查询搭建师:通路 A 直调 dataclaw-service HTTP,不挂任何数据 MCP。daemon refreshDaemonManagedDirs 会把本文件同步到 ~/.clawd/personas/persona-dataclaw-builder/.mcp.json。github:GitHub 官方远程 MCP,建仓 / 提交文件由 agent 按 .claude/skills/app-builder-projects 走它(与 persona-app-builder 同一份 skill),本机不依赖 gh CLI。`${GITHUB_TOKEN}` 是保留变量名 = 当前用户的 GitHub 身份(spec 2026-09-15-github-auth-unify §4.1):本机由 daemon 从机器级身份(设置 → 账号关联)渲进 0600 产物的 headers,云上由中心从使用者的 GitHub 身份供值;为什么必须是占位见 persona-app-builder/.mcp.json 的说明。",
2
+ "_comment": "数据查询搭建师:通路 A 直调 dataclaw-service HTTP,不挂任何数据 MCP。daemon refreshDaemonManagedDirs 会把本文件同步到 ~/.clawd/personas/persona-dataclaw-builder/.mcp.json。github:GitHub 官方远程 MCP,建仓 / 提交文件由 agent 按 .claude/skills/app-builder-projects 走它(与 persona-app-builder 同一份 skill),本机不依赖 gh CLI。`${GITHUB_TOKEN}` 是保留变量名 = 当前用户的 GitHub 身份(spec 2026-09-15-github-auth-unify §4.1):本机由 daemon 从机器级身份(设置 → 账号关联)渲进 0600 产物的 headers,云上由中心从使用者的 GitHub 身份供值;为什么必须是占位见 persona-app-builder/.mcp.json 的说明。 clawd-app-builder:本 persona 自己的 stdio MCP(design 2026-09-19-persona-mcp-single-source §3.2)——项目 scaffold / 发布 / 拆除三件必须持宿主凭据的事在它进程里做。代码在本目录 `mcp/clawd-app-builder/`(mcp-server.cjs + deploy-kit/,daemon 构建时由 copy-defaults 放进 persona-defaults、每次启动整份换新);args 里的 `${CLAWD_PERSONA_DIR}` 是 args 唯一放行的占位(persona 目录不是凭据),本机 daemon / 云上 pod-setup 渲成绝对路径;使用者工作区不在这里写,插件读宿主 env `CLAWD_WORK_DIR`。env 只列它要的四个:kit 模板渲进项目 .env / FC 环境的 SUPABASE_URL / SUPABASE_KEY(anon key),publish / remove 脚本要的两个阿里云 AK——本机放 ~/.clawd/secrets/{supabase,aliyun}.env,云上来自仓库 Secrets。",
3
3
  "mcpServers": {
4
4
  "github": {
5
5
  "type": "http",
@@ -7,6 +7,19 @@
7
7
  "headers": {
8
8
  "Authorization": "Bearer ${GITHUB_TOKEN}"
9
9
  }
10
+ },
11
+ "clawd-app-builder": {
12
+ "type": "stdio",
13
+ "command": "node",
14
+ "args": [
15
+ "${CLAWD_PERSONA_DIR}/mcp/clawd-app-builder/mcp-server.cjs"
16
+ ],
17
+ "env": {
18
+ "SUPABASE_URL": "${SUPABASE_URL}",
19
+ "SUPABASE_KEY": "${SUPABASE_KEY}",
20
+ "ALIBABA_CLOUD_ACCESS_KEY_ID": "${ALIBABA_CLOUD_ACCESS_KEY_ID}",
21
+ "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "${ALIBABA_CLOUD_ACCESS_KEY_SECRET}"
22
+ }
10
23
  }
11
24
  }
12
25
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 工作流(app-builder-projects skill + clawd-app-builder MCP + github MCP)
6
6
 
7
- 你复用的是 app-builder 那条项目流水线(persona 无关),**不是** app-builder persona 的磁盘资产——你摸不到也不需要它。下面是主线;步骤细节以你自己目录里的 `.claude/skills/app-builder-projects/SKILL.md` 为准。
7
+ 你复用的是 app-builder 那条项目流水线(persona 无关):`clawd-app-builder` MCP server 与它的 deploy-kit 在你自己目录的 `mcp/clawd-app-builder/`,与 app-builder persona 各一份,不共享磁盘资产。下面是主线;步骤细节以你自己目录里的 `.claude/skills/app-builder-projects/SKILL.md` 为准。
8
8
 
9
9
  一个 app = 当前用户工作区下的 `projects/<name>/`,配一个 GitHub 组织 `ottin4ttc` 下的私有仓库(`<name>-<slug>`),仓库由你经 **`github` MCP** 建和提交,本机不需要 `gh`、项目里不需要 git。会话不绑项目:一个会话里可以改多个 app,隔几天新开会话改旧 app 用 skill 的 `list-projects.mjs` 找目录。没有右栏预览、没有 dev server 概念,验证靠发布后的公网链接。
10
10
 
@@ -6,15 +6,14 @@
6
6
 
7
7
  FC 对应用只有一条要求:**一个监听 `$PORT` 的 HTTP server**。所以固化的是「发布管道」,不是技术栈。
8
8
 
9
- - **共享 deploy-kit(凭证 + 脚本骨架 + bootstrap,全 FC persona 一份)**:`$HOME/.clawd/deploy-kit/`(`scripts/` + `contract/bootstrap` + `.secrets/aliyun.env.local`)
9
+ - **deploy-kit(脚本骨架 + bootstrap)**:本 persona 的 `mcp/clawd-app-builder/deploy-kit/`(`scripts/` + `contract/bootstrap`,与 `clawd-app-builder` MCP server 同目录,daemon 每次启动整份换新)。阿里云 AK 不在这里:走凭据表 `~/.clawd/secrets/aliyun.env`,daemon 渲进 MCP server 的 env
10
10
  - **persona 特化(本目录)**:`config.env`(dataclaw 注入值) + `contract/s.yaml.tmpl`(占位符清单) + `hooks/pre-deploy.sh`(发布前置) + `examples/`(默认 `nestjs-react`)
11
11
 
12
12
  ## 目录
13
13
 
14
14
  ```
15
- # 共享 deploy-kit($HOME/.clawd/deploy-kit/,daemon 维护,全 persona 共用)
15
+ # 本 persona 的 deploy-kit($HOME/.clawd/personas/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/,daemon 维护)
16
16
  deploy-kit/
17
- ├── .secrets/aliyun.env.local 阿里云凭证(单源,使用者自己写;bundle 只带 *.example)
18
17
  ├── contract/bootstrap FC 启动入口(Node 参考实现:find node + cd + exec)
19
18
  └── scripts/
20
19
  ├── new-extension.sh 从示例生成新 extension(纯 copy,daemon 自动跑)
@@ -32,10 +31,10 @@ extension-kit/
32
31
 
33
32
  ## 用法
34
33
 
35
- 日常 happy path 由 daemon 自动跑(createProject 内联 scaffold、「发布上线」按钮触发 publish)。需手动跑时用共享 deploy-kit 绝对路径,**第二参是本 persona 根**:
34
+ 日常 happy path 由 daemon 自动跑(createProject 内联 scaffold、「发布上线」按钮触发 publish)。需手动跑时用本 persona 目录下 deploy-kit 的绝对路径,**第二参是本 persona 根**:
36
35
 
37
36
  ```bash
38
- PR="$HOME/.clawd/personas/persona-dataclaw-builder"; DK="$HOME/.clawd/deploy-kit"
37
+ PR="$HOME/.clawd/personas/persona-dataclaw-builder"; DK="$HOME/.clawd/personas/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit"
39
38
 
40
39
  # 1) scaffold(daemon 已自动跑;手动签名: new-extension.sh <name> <模板源目录> <目标目录>)
41
40
  # 2) 改 web/ 和 server/ 业务逻辑(只读查 dataclaw 公网 API,不接 Supabase 写库)
@@ -57,7 +56,7 @@ PR="$HOME/.clawd/personas/persona-dataclaw-builder"; DK="$HOME/.clawd/deploy-kit
57
56
  ## 清理一个 extension
58
57
 
59
58
  ```bash
60
- "$HOME/.clawd/deploy-kit/scripts/remove-extension.sh" <projDir> "$HOME/.clawd/personas/persona-dataclaw-builder"
59
+ "$HOME/.clawd/personas/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/remove-extension.sh" <projDir> "$HOME/.clawd/personas/persona-dataclaw-builder"
61
60
  ```
62
61
 
63
62
  `s remove` 删 FC 函数+触发器+自定义域名。
@@ -82,7 +81,7 @@ PR="$HOME/.clawd/personas/persona-dataclaw-builder"; DK="$HOME/.clawd/deploy-kit
82
81
 
83
82
  ### ⚠️ bootstrap 归 daemon 管,改了会被还原
84
83
 
85
- `publish.sh` 用的是**共享 deploy-kit** 那份:`$HOME/.clawd/deploy-kit/contract/bootstrap`
84
+ `publish.sh` 用的是**本 persona 的 deploy-kit** 那份:`mcp/clawd-app-builder/deploy-kit/contract/bootstrap`
86
85
  (不是本目录下的)。而 `deploy-kit/contract/` 和本 `extension-kit/` 一样,**每次 daemon 启动
87
86
  都会被 bundle 版本无条件覆盖**——就地改它,下次重启就没了,而且没有 `.secrets/*.local`
88
87
  那样的个人 override 豁免。
@@ -1,5 +1,5 @@
1
1
  # ===== 平台级固定变量(一次配好,所有 extension 共用)=====
2
- # 阿里云凭证不在这里:从 .secrets/aliyun.env.local 现读(见 CLAUDE.md)
2
+ # 阿里云凭证不在这里:走凭据表 ~/.clawd/secrets/aliyun.env,daemon 渲进 clawd-app-builder MCP server 的 env(见 CLAUDE.md)
3
3
 
4
4
  REGION=cn-hangzhou
5
5
 
@@ -9,9 +9,10 @@
9
9
  # __TABLE_PREFIX__ = APP_NAME_<slug>_[int_](本 persona 不用 supabase,随共享 publish.sh 一起注入)
10
10
  edition: 3.0.0
11
11
  name: __DEPLOY_NAME__-app
12
- # clawd 自己的 Serverless Devs profile,写在 deploy-kit 私有 s-home 里(见 scripts/ensure-credentials.sh)。
12
+ # clawd 自己的 Serverless Devs access:s 从进程 env(AccountID / AccessKeyID / AccessKeySecret)合成的那份,
13
+ # 固定叫 $system_environment_access(见 deploy-kit/scripts/ensure-credentials.sh);publish.sh 调 s 时还会显式 --access 覆盖这里。
13
14
  # 刻意不用 default —— 那是用户自己账号的 profile,不该被 clawd 读也不该被 clawd 写。
14
- access: clawd-fc
15
+ access: $system_environment_access
15
16
 
16
17
  vars:
17
18
  region: __REGION__
@@ -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,96 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # 发布/清理动手前把阿里云凭证准备好(publish.sh / remove-extension.sh 共用)。
4
+ # 被 `source` 进来,再调 ensure_credentials。跟 ensure-toolchain.sh 是一对:
5
+ # 那个管「命令在不在」,这个管「命令拿什么身份跑」。
6
+ #
7
+ # 硬约束:**全程不碰用户全局 ~/.s,凭证不落盘**。clawd 发布用的是 clawd 自己那份 AK(本机 ~/.clawd/secrets/aliyun.env 经 daemon 渲进
8
+ # MCP server env,云上是 pod-setup 渲的 env),而 ~/.s/access.yaml 里的 default 是用户自己账号的
9
+ # Serverless Devs 凭证——它是用户的东西,不是我们的。做法对齐本 kit 处理 aliyun CLI 的方式(凭证只进本次进程的 env):
10
+ # s v3 的 @serverless-devs/credential 有 getEnvKeyPair——进程 env 里同时有 AccountID / AccessKeyID / AccessKeySecret 三个变量,
11
+ # 就自动合成一个叫 `$system_environment_access` 的 access(实测 s 3.1.10 源码),不需要任何 access.yaml。
12
+ # Serverless Devs 的配置家目录仍指到 deploy-kit 名下,但那里只剩组件缓存和日志,没有凭证。
13
+ # ============================================================
14
+
15
+ # s 的 access 名 = s 自己给「从 env 合成的那份」起的固定名字(字面量,$ 不是 shell 变量)。刻意不用 default:
16
+ # env 没齐时 s 找不到这个名字就 `Not found access: $system_environment_access` 硬报错,而不是默默拿用户
17
+ # ~/.s/access.yaml 里的 default 凭证把 extension 部署进别人的账号。调 s 时都显式带 `--access "$S_ACCESS_ALIAS"`:
18
+ # 命令行参数覆盖 s.yaml 里的 access 字段,老项目(s.yaml 写着 clawd-fc / default)照样走这条。
19
+ S_ACCESS_ALIAS='$system_environment_access'
20
+
21
+ # 阿里云 AK 只从进程 env 来(design 2026-09-19-persona-mcp-single-source §3.2):本脚本由 clawd-app-builder MCP server 起,
22
+ # AK 在那个进程的 env 里——本机由 daemon 从凭据表(~/.clawd/secrets/aliyun.env)渲进 .mcp.json 声明的 per-server env,
23
+ # 云上由 pod-setup 从仓库 Secrets 渲进 config.toml 的 [mcp_servers.clawd-app-builder].env。不落盘是有意的:
24
+ # 写成文件的话 agent 一个 `cat` 就看见了。deploy-kit 目录下曾经的 `.secrets/aliyun.env[.local]` 两条读法已删。
25
+ #
26
+ # clawd **不随包 ship 任何一把 AK**(2026-09-18 起,见 specs/2026-09-18-aliyun-ak-leak-rotate-design.md:
27
+ # 上一把随 npm 包发出去 69 秒就被 TruffleHog 收割,主账号被风控锁)。env 里没有 = 使用者还没配,fail-loud。
28
+ ensure_aliyun_env() {
29
+ if [ -z "${ALIBABA_CLOUD_ACCESS_KEY_ID:-}" ] || [ -z "${ALIBABA_CLOUD_ACCESS_KEY_SECRET:-}" ]; then
30
+ echo "❌ 没有阿里云 AK:本机把 ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET 两行写进 ~/.clawd/secrets/aliyun.env(daemon 起 clawd-app-builder 时渲进它的 env);云上由 gateway 注入。" >&2
31
+ return 1
32
+ fi
33
+ export ALIBABA_CLOUD_REGION_ID="${REGION:?config.env 缺 REGION}"
34
+ }
35
+
36
+ # 调 aliyun CLI 的**唯一入口**:把凭证和 region 作为显式参数交出去,不依赖环境变量的隐式优先级。
37
+ #
38
+ # 为什么非这样不可(实测 aliyun 3.3.17,`--mode AK`):`ALIBABA_CLOUD_ACCESS_KEY_ID` /
39
+ # `_SECRET` / `_REGION_ID` 这三个环境变量在这个模式下**根本不被消费**,于是
40
+ # - 机器上没有 `~/.aliyun/config.json`(全新用户的常态,ensure_toolchain 只装 CLI、
41
+ # 从不跑 `aliyun configure`)→ `ERROR: region can't be empty`,第一次发布就跑不起来;
42
+ # - 机器上有 profile(比如用户自己配过阿里云)→ **静默走用户自己的 profile**,等于拿
43
+ # 用户的阿里云身份去部署 extension,跟 access.yaml 那个坑是同一类。
44
+ # 补 `--region` 一个 flag 不够,AK 也必须是 flag——两者都试过。
45
+ #
46
+ # 用法跟 aliyun 一样,省掉 `--mode AK` 和三个凭证参数:aliyun_ak sts GetCallerIdentity
47
+ aliyun_ak() {
48
+ aliyun --mode AK \
49
+ --access-key-id "${ALIBABA_CLOUD_ACCESS_KEY_ID:?aliyun_ak 需要先 ensure_aliyun_env}" \
50
+ --access-key-secret "${ALIBABA_CLOUD_ACCESS_KEY_SECRET:?aliyun_ak 需要先 ensure_aliyun_env}" \
51
+ --region "${ALIBABA_CLOUD_REGION_ID:?aliyun_ak 需要先 ensure_aliyun_env}" \
52
+ "$@"
53
+ }
54
+
55
+ # 把 s 要的三个变量放进本进程 env(AccountID 从 GetCallerIdentity 现取),并把 Serverless Devs 的配置家目录
56
+ # (组件缓存 / 日志)指到 deploy-kit 名下——不写 access.yaml,凭证不落盘。
57
+ # s v3 的 rootHome = `$serverless_devs_config_home/.s`(默认 `$HOME/.s`),`s -v` 打印的 s-home 就是它。
58
+ # 前置:ensure_toolchain(aliyun 可执行)+ ensure_aliyun_env(AK 进 env)。
59
+ # 家目录放 $KIT_DIR 下(= persona 目录的 mcp/clawd-app-builder/deploy-kit/.s-home/):只有缓存、没有秘密;
60
+ # persona 仓库的 gitignore 模板(daemon deploy/templates.ts GITIGNORE_LINES)忽略 .s-home/(缓存不该进仓库);
61
+ # 本机每次 daemon 启动 mcp/ 整份换新,组件缓存跟着清,下次 publish 重装一次。
62
+ ensure_s_access() {
63
+ local kit="${KIT_DIR:?ensure_s_access 需要调用方先设 KIT_DIR}"
64
+ local s_home="$kit/.s-home"
65
+ local ident acc
66
+
67
+ ident="$(aliyun_ak sts GetCallerIdentity 2>&1)" || {
68
+ # 别只说「检查凭证」——凭证不对只是可能之一,region 缺失 / CLI 装坏 / 网络不通报的是别的话。
69
+ # 把 CLI 原文摆出来,让读的人按原文判断。
70
+ echo "❌ aliyun sts GetCallerIdentity 失败,拿不到 AccountId。aliyun 原文:" >&2
71
+ echo "$ident" >&2
72
+ echo " 排查顺序:① 上面这行 CLI 原文说的是什么 ② 进程 env 里的 AK 是否有效(本机 ~/.clawd/secrets/aliyun.env、云上仓库 Secrets)——2026-09-18 之前 OTA 发的那把已泄露、不可再用,得换自己的 ③ config.env 的 REGION" >&2
73
+ return 1
74
+ }
75
+ # 用 node 不用 python3:目标机器保证有 node(clawd 本身就是 node 跑的),不保证有 python
76
+ # —— 清理路径尤其不该多一个装不上就跑不了的依赖。
77
+ acc="$(printf '%s' "$ident" | node -e 'console.log(JSON.parse(require("fs").readFileSync(0,"utf8")).AccountId)')" || {
78
+ echo "❌ 解析 AccountId 失败,aliyun 输出非 JSON:" >&2
79
+ echo "$ident" >&2
80
+ return 1
81
+ }
82
+
83
+ # s 认的三个变量名就是它 access.yaml 里的键名(KEY_PAIR_IMPORTANT),大小写照它的
84
+ export AccountID="$acc"
85
+ export AccessKeyID="$ALIBABA_CLOUD_ACCESS_KEY_ID"
86
+ export AccessKeySecret="$ALIBABA_CLOUD_ACCESS_KEY_SECRET"
87
+ export serverless_devs_config_home="$s_home"
88
+ mkdir -p "$s_home/.s"
89
+ # 老版本脚本(2026-09-19 之前)把凭证写在这里;本机残留的顺手删掉,别让一份过期 AK 躺在 persona 目录里
90
+ rm -f "$s_home/.s/access.yaml"
91
+ }
92
+
93
+ ensure_credentials() {
94
+ ensure_aliyun_env
95
+ ensure_s_access
96
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # 确保发布/清理用到的工具链就绪:Serverless Devs (s) + 阿里云 CLI (aliyun)。
4
+ # 检测环境、缺啥装啥、已装则跳过(幂等)。被 publish.sh / remove-extension.sh
5
+ # 在调用 aliyun/s 之前 `source` 进来,再调 ensure_toolchain。
6
+ #
7
+ # 为什么需要它:FC 发布全靠 aliyun CLI + s 两个外部命令,二者都不随 clawd 安装。
8
+ # 新机器上它们不在 PATH → ensure-credentials.sh 的 `aliyun_ak sts ...` stdout 为空 →
9
+ # JSON.parse 空串报错,且死在第一个 ::stage:: marker 之前 → daemon 只能报 [unknown] 阶段。
10
+ # 这个脚本补的就是这一环。
11
+ #
12
+ # 注意它**只装不配**:不会跑 `aliyun configure`,所以机器上不会因此有 `~/.aliyun/config.json`
13
+ # ——凭证由 ensure-credentials.sh 的 aliyun_ak 每次显式传参,不依赖任何本机 profile。
14
+ # ============================================================
15
+
16
+ # aliyun CLI 官方 release 兜底版本(仅在无 brew、走二进制下载时用;brew 路径不读它)。
17
+ CLAWD_ALIYUN_CLI_VERSION="${CLAWD_ALIYUN_CLI_VERSION:-3.3.21}"
18
+
19
+ ensure_s() {
20
+ command -v s >/dev/null 2>&1 && return 0
21
+ echo "==> 安装 Serverless Devs (s)…"
22
+ command -v npm >/dev/null 2>&1 || { echo "❌ 缺 npm,无法安装 @serverless-devs/s" >&2; return 1; }
23
+ npm install -g @serverless-devs/s
24
+ }
25
+
26
+ ensure_aliyun() {
27
+ command -v aliyun >/dev/null 2>&1 && return 0
28
+ echo "==> 安装阿里云 CLI (aliyun)…"
29
+ # 优先 brew(有 brew 的 mac 最省事,装到已在 PATH 的 /opt/homebrew/bin | /usr/local/bin)
30
+ if command -v brew >/dev/null 2>&1; then
31
+ brew install aliyun-cli
32
+ return 0
33
+ fi
34
+ # 兜底:官方 release 二进制(没装 brew 的新机器 / Linux server)。
35
+ local os arch bindir="$HOME/.clawd/bin"
36
+ case "$(uname -s)" in
37
+ Darwin) os=macosx ;;
38
+ Linux) os=linux ;;
39
+ *) echo "❌ 不支持的系统 $(uname -s),请手动安装 aliyun CLI" >&2; return 1 ;;
40
+ esac
41
+ case "$(uname -m)" in
42
+ arm64|aarch64) arch=arm64 ;;
43
+ x86_64|amd64) arch=amd64 ;;
44
+ *) echo "❌ 不支持的架构 $(uname -m),请手动安装 aliyun CLI" >&2; return 1 ;;
45
+ esac
46
+ local ver="$CLAWD_ALIYUN_CLI_VERSION"
47
+ local url="https://github.com/aliyun/aliyun-cli/releases/download/v${ver}/aliyun-cli-${os}-${ver}-${arch}.tgz"
48
+ echo " 无 brew,下载官方二进制: $url"
49
+ mkdir -p "$bindir"
50
+ curl -fsSL "$url" | tar xz -C "$bindir"
51
+ chmod +x "$bindir/aliyun"
52
+ export PATH="$bindir:$PATH"
53
+ }
54
+
55
+ ensure_toolchain() {
56
+ ensure_s
57
+ ensure_aliyun
58
+ }
59
+
60
+ # 直接执行(bash ensure-toolchain.sh)= 自检;被 source 时不自动跑,留给调用方显式 ensure_toolchain。
61
+ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
62
+ set -euo pipefail
63
+ ensure_toolchain
64
+ echo "✅ 工具链就绪: s=$(command -v s) aliyun=$(command -v aliyun)"
65
+ fi
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # 从模板源目录展开一个新 extension(通用,persona 无关)。
3
+ # 用法: new-extension.sh <name> <模板源目录绝对路径> <目标项目目录绝对路径>
4
+ # - 模板源目录:persona 的 extension-kit/examples/<tmpl>(daemon 按 persona 解析后传入)
5
+ # - 目标目录:daemon createProject 已建好(含 .clawd-project.json)的 projects/<name>/
6
+ set -euo pipefail
7
+
8
+ NAME="${1:?用法: new-extension.sh <name> <模板源目录> <目标目录>}"
9
+ SRC="${2:?缺模板源目录}"
10
+ DEST="${3:?缺目标目录}"
11
+
12
+ [ -d "$SRC" ] || { echo "❌ 模板源目录不存在: $SRC" >&2; exit 1; }
13
+ [ -d "$DEST" ] && [ -f "$DEST/.clawd-project.json" ] || {
14
+ echo "❌ 目标目录无效(必须已存在且含 .clawd-project.json): $DEST" >&2; exit 1
15
+ }
16
+
17
+ # 目标须只含 .clawd-project.json(daemon 预建),否则视为脏目录拒绝
18
+ remaining="$(find "$DEST" -mindepth 1 -maxdepth 1 ! -name '.clawd-project.json' -print -quit)"
19
+ [ -z "$remaining" ] || { echo "❌ 目标已有内容(非空目录): $DEST" >&2; exit 1; }
20
+
21
+ # 排除 node_modules:模板可能被 install 过,pnpm 的 node_modules 里有指向**模板绝对路径**的软链
22
+ # (tslib 等传递依赖)。cp 到新项目后这些软链 dangling(仍指模板原始路径);而 pnpm install 见
23
+ # node_modules 已在 + lockfile 没变会直接 skip、不重建软链 → 运行/打包时 Cannot find module。
24
+ # 新项目本来就要 install,故 scaffold 一律不拷 node_modules(保留 lockfile 以锁版本)。
25
+ rsync -a --exclude='node_modules' "$SRC/" "$DEST/"
26
+
27
+ # 全局唯一 slug (4 hex chars, ~65k 散列空间):同名 app 多用户/多副本部署到共享 FC + 共享
28
+ # Supabase 时,FC functionName / 子域名 / SQL 表前缀都得唯一。一次 scaffold 锁定,
29
+ # publish.sh 和 remove-extension.sh 都 source ext.conf 共用同一个 slug。
30
+ # Agent 命名 Supabase 表时务必带上: <APP_NAME>_<SLUG>_<table>(见 CLAUDE.md「后端:supabase」)。
31
+ SLUG="$(openssl rand -hex 2)"
32
+ {
33
+ echo ""
34
+ echo "# === 全局唯一身份 (scaffold 时锁定,不要改) ==="
35
+ echo "# APP_NAME = 项目名 (人类可读身份);SLUG = 4 位散列防同名冲突"
36
+ echo "# 派生:DEPLOY_NAME=\${APP_NAME}-\${SLUG}[-int] (FC 函数/子域名,DNS-safe 用连字符;integration 带 -int)"
37
+ echo "# 表前缀=\${APP_NAME}_\${SLUG}_[int_] (SQL-safe 用下划线;integration 带 int_,server 经 env TABLE_PREFIX 取)"
38
+ echo "APP_NAME=${NAME}"
39
+ echo "SLUG=${SLUG}"
40
+ } >> "$DEST/ext.conf"
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
+ # 本地 pnpm dev 一律连 integration 那套表(spec 2026-09-15-app-builder-integration-env-design §3.4):
66
+ # 这两个值在渲 .env 之前就地定死,不从进程 env 取——scaffold 的环境里就算有同名变量也不该漏进来。
67
+ export APP_ENV=integration
68
+ export TABLE_PREFIX="${NAME}_${SLUG}_int_"
69
+ while IFS= read -r f; do render_env_example "$f"; done < <(find "$DEST" -name '*.env.example' -not -path '*/node_modules/*')
70
+
71
+ echo "✅ 新 extension: $DEST (APP_NAME=${NAME}, SLUG=${SLUG})"
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # 通用一键发布 extension 到阿里云 FC(persona 无关)
4
+ # 用法: publish.sh <extension目录> <persona根目录> <env>
5
+ # - <persona根目录>: daemon 按 session persona 解析后传入(.../personas/<id>)
6
+ # - <env>: integration | prod(必填,无默认)。integration 发到 <APP_NAME>-<SLUG>-int(测试地址),
7
+ # prod 发到 <APP_NAME>-<SLUG>(老地址不变)。见 spec 2026-09-15-app-builder-integration-env-design
8
+ # - 脚本骨架/bootstrap 来自本脚本所在的 deploy-kit(persona 目录 mcp/clawd-app-builder/ 下);平台凭证在进程 env
9
+ # - persona 注入值/占位符/特化逻辑/persona 密钥来自 <persona根>/extension-kit
10
+ # ============================================================
11
+ set -euo pipefail
12
+
13
+ KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)" # deploy-kit 根(persona 目录 mcp/clawd-app-builder/deploy-kit/)
14
+ EXT_DIR="${1:?用法: publish.sh <extension目录> <persona根目录> <env>}"
15
+ EXT_DIR="$(cd "$EXT_DIR" && pwd)"
16
+ PERSONA_ROOT="${2:?缺 persona 根目录}"
17
+ PERSONA_KIT="$PERSONA_ROOT/extension-kit"
18
+
19
+ # 1) 配置:persona config.env(含平台值+注入值)+ extension 自身 ext.conf(覆盖)
20
+ [ -f "$PERSONA_KIT/config.env" ] && source "$PERSONA_KIT/config.env"
21
+ [ -f "$EXT_DIR/ext.conf" ] && source "$EXT_DIR/ext.conf"
22
+ # env 在 source 之后赋值:位置参数是唯一来源,config.env / ext.conf 里就算写了同名变量也盖不过它
23
+ APP_ENV="${3:?缺 env(integration|prod)}"
24
+ case "$APP_ENV" in integration|prod) ;; *) echo "❌ env 只能是 integration|prod,收到:$APP_ENV" >&2; exit 1;; esac
25
+ APP_NAME="${APP_NAME:-$(basename "$EXT_DIR")}"
26
+ CODE_DIR="${CODE_DIR:-./server}"
27
+ FC_ENTRY="${FC_ENTRY:-dist/main.js}"
28
+ BUILD_CMD="${BUILD_CMD:-}"
29
+
30
+ # 全局唯一身份:APP_NAME (人读身份/表前缀根) + SLUG (4 位散列)。一次性 scaffold 时
31
+ # 由 new-extension.sh 锁定 + bake 进 ext.conf。这里的 fallback 兜底两种历史情况:
32
+ # 1) 老项目 (本改动前 scaffold) ext.conf 无 SLUG → 现场生成 + bake,保后续稳定
33
+ # 2) 手动 rm SLUG 或损坏 ext.conf → 同上,fail-safe
34
+ # 派生 (按 env):
35
+ # DEPLOY_NAME=${APP_NAME}-${SLUG}[-int] → FC functionName/子域名/s 项目名 (DNS-safe 连字符)
36
+ # TABLE_PREFIX=${APP_NAME}_${SLUG}_[int_] → Supabase 表 (SQL-safe 下划线,见 remove-extension.sh)
37
+ if [ -z "${SLUG:-}" ]; then
38
+ SLUG="$(openssl rand -hex 2)"
39
+ printf '\nSLUG=%s\n' "$SLUG" >> "$EXT_DIR/ext.conf"
40
+ echo " 📝 锁定 SLUG=$SLUG (含 4 位散列防同名冲突,已 bake 进 ext.conf)"
41
+ fi
42
+ BASE_NAME="${APP_NAME}-${SLUG}"
43
+ # 环境名 integration / prod 是人读的;进资源名的是短标签 ENV_TAG:int / (prod 为空,保持老地址不变)。
44
+ # 一眼看域名:带 -int 的就是测试环境。派生段与 publish.spec.ts 的 DERIVE_SNIPPET 同步。
45
+ if [ "$APP_ENV" = integration ]; then
46
+ ENV_TAG=int; DEPLOY_NAME="${BASE_NAME}-int"; TABLE_PREFIX="${APP_NAME}_${SLUG}_int_"
47
+ else
48
+ ENV_TAG=prod; DEPLOY_NAME="${BASE_NAME}"; TABLE_PREFIX="${APP_NAME}_${SLUG}_"
49
+ fi
50
+ # 两个环境各自的渲染产物;s 用 name: 键状态,DEPLOY_NAME 不同即隔离。
51
+ S_FILE="s.${ENV_TAG}.yaml"
52
+ echo " 🎯 env=$APP_ENV → $DEPLOY_NAME (表前缀 $TABLE_PREFIX, 渲到 $S_FILE)"
53
+ # 渲出来的 s 文件含 supabase key,靠项目 .gitignore 挡在仓库外。模板已有 s.*.yaml;双环境之前 scaffold 的
54
+ # 存量项目只有 s.yaml 一行,s.int.yaml 会被当源码推上 GitHub——像 bake SLUG 一样幂等地补一行(改了
55
+ # .gitignore 下次 publish 会被「先推再发」拦一次,推上去的只是 .gitignore,无害)。
56
+ if ! grep -qxF 's.*.yaml' "$EXT_DIR/.gitignore" 2>/dev/null; then
57
+ printf 's.*.yaml\n' >> "$EXT_DIR/.gitignore"
58
+ echo " 📝 .gitignore 补 s.*.yaml (渲出的 s.int.yaml / s.prod.yaml 含 key,不进仓库;记得把 .gitignore 推上去)"
59
+ fi
60
+
61
+ # 2) 工具链 + 阿里云凭证(进程 env,本机云上同一条;见 ensure-credentials.sh)
62
+ source "$KIT_DIR/scripts/ensure-toolchain.sh"
63
+ ensure_toolchain
64
+ source "$KIT_DIR/scripts/ensure-credentials.sh"
65
+ ensure_credentials
66
+
67
+ # 3) persona hook(可选):校验/导出 persona 特化运行时变量(如 SESSION_SECRET / APP_BASE_URL / persona 密钥)
68
+ if [ -f "$PERSONA_KIT/hooks/pre-deploy.sh" ]; then
69
+ source "$PERSONA_KIT/hooks/pre-deploy.sh"
70
+ fi
71
+
72
+ # 4) build(框架相关)
73
+ echo "::stage::build"
74
+ if [ -n "$BUILD_CMD" ]; then
75
+ echo "==> build: $BUILD_CMD"
76
+ ( cd "$EXT_DIR" && eval "$BUILD_CMD" )
77
+ fi
78
+
79
+ # 5) bootstrap(共享,框架无关)
80
+ cp "$KIT_DIR/contract/bootstrap" "$EXT_DIR/$CODE_DIR/bootstrap"
81
+ chmod +x "$EXT_DIR/$CODE_DIR/bootstrap"
82
+
83
+ # 6) 通用占位符替换渲染 s.yaml:扫 persona s.yaml.tmpl 的所有 __X__,从 env 取同名变量
84
+ #(APP_ENV / TABLE_PREFIX 也走这条:模板里 __APP_ENV__ / __TABLE_PREFIX__ 渲进 FC 的 environmentVariables)
85
+ #
86
+ # 自定义域名段按履约拼接(M1b Step 5c):APP_DOMAIN_SUFFIX 非空 = 部署级履约(域名与通配符
87
+ # 证书都在我们自己的阿里云账号上)→ 把 s.yaml.domain.tmpl 接上;空 = 使用者自带 key,
88
+ # 发布落在他自己的账号里,那里既没有我们的 DNS 也没有我们的证书 → 整段不渲染,用 FC 默认域名。
89
+ # 拼完再走同一套占位符替换,渲染契约本身不分叉。
90
+ TMPL="$PERSONA_KIT/contract/s.yaml.tmpl"
91
+ [ -f "$TMPL" ] || { echo "❌ 缺 $TMPL" >&2; exit 1; }
92
+ # **判据是「这个 persona 有没有 s.yaml.domain.tmpl」**,不是光看 APP_DOMAIN_SUFFIX:
93
+ # 没有那个文件的 persona(persona-dataclaw-builder)把自定义域名段写死在自己的 s.yaml.tmpl 里,
94
+ # 它压根不参与这套切换,替它选分支只会把它的发布路径改坏。
95
+ DOMAIN_TMPL="$PERSONA_KIT/contract/s.yaml.domain.tmpl"
96
+ TMPL_RENDER="$TMPL"
97
+ TMPL_COMBINED=""
98
+ if [ -f "$DOMAIN_TMPL" ]; then
99
+ if [ -n "${APP_DOMAIN_SUFFIX:-}" ]; then
100
+ # mktemp 落 EXT_DIR 之外:占位符缺失时下面直接 exit,别在用户项目目录里留残骸。
101
+ # **必须写全 XXXXXX 模板**:BSD mktemp(macOS)的 `-t prefix` 会自己补随机后缀,
102
+ # GNU coreutils(Linux,= M1b 的 Pod)要求模板以 ≥3 个 X 结尾,`mktemp -t s-yaml-tmpl`
103
+ # 直接 `too few X's in template` 退 1(coreutils 9.1 实测),`set -e` 下当场打死这条
104
+ # 默认路径。deploy-kit 的脚本两套 userland 都要跑,别用 BSD/GNU 语义有分歧的写法。
105
+ TMPL_COMBINED="$(mktemp "${TMPDIR:-/tmp}/s-yaml-tmpl.XXXXXX")"
106
+ trap 'rm -f "$TMPL_COMBINED"' EXIT
107
+ cat "$TMPL" "$DOMAIN_TMPL" > "$TMPL_COMBINED"
108
+ TMPL_RENDER="$TMPL_COMBINED"
109
+ echo " 🌐 自定义域名段:${DEPLOY_NAME}.${APP_DOMAIN_SUFFIX}"
110
+ else
111
+ echo " 🌐 未配 APP_DOMAIN_SUFFIX(使用者自带 key 履约):跳过自定义域名段,用 FC 默认域名"
112
+ fi
113
+ fi
114
+ sed_args=()
115
+ for ph in $(grep -oE '__[A-Z0-9_]+__' "$TMPL_RENDER" | sort -u); do
116
+ var="${ph#__}"; var="${var%__}"
117
+ # declare -p 测「是否定义」(含空字符串),兼容 bash 3.2;未定义即 fail-loud
118
+ if ! declare -p "$var" >/dev/null 2>&1; then
119
+ echo "❌ s.yaml.tmpl 占位符 $ph 无对应 env 变量 \$$var" >&2
120
+ # 最常见的成因不是「漏配」,而是**这个脚本被手动跑了**:凭据由 daemon 在 spawn 时注进
121
+ # 它起的那个子进程,别的地方起的 shell 拿不到。把这条写进报错里,是因为读它的下一个
122
+ # 读者往往是 agent——不给方向它就会去翻凭据、手动 export 或写回 config.env,
123
+ # 正好把「值不落 persona 目录」这条纪律拆掉。
124
+ echo " 如果你是手动跑这个脚本:凭据只注给 daemon 起的那个进程,手动跑必然缺。" >&2
125
+ echo " 正确做法是改调 clawd-app-builder 的 publish tool 重跑;**不要**去找 key 手动 export,也不要写回 config.env。" >&2
126
+ echo " 确实是漏配(新增了占位符 / 换了实例):改 daemon 的凭据表(~/.clawd/secrets/),不是改模板。" >&2
127
+ exit 1
128
+ fi
129
+ # 转义 sed 替换串里的特殊字符(密钥/URL 可能含 \ | &),顺序:先 \ 再 | / &
130
+ val="${!var}"
131
+ val="${val//\\/\\\\}"
132
+ val="${val//|/\\|}"
133
+ val="${val//&/\\&}"
134
+ sed_args+=(-e "s|$ph|$val|g")
135
+ done
136
+ sed "${sed_args[@]}" "$TMPL_RENDER" > "$EXT_DIR/$S_FILE"
137
+
138
+ # 7) deploy + verify
139
+ echo "::stage::deploy"
140
+ echo "==> s deploy ($DEPLOY_NAME, env=$APP_ENV)"
141
+ ( cd "$EXT_DIR" && s deploy -y -t "$S_FILE" --access "$S_ACCESS_ALIAS" )
142
+
143
+ echo "::stage::verify"
144
+ # 产物 URL 怎么取,取决于**这个 persona 参不参与 5c 的域名段方案**(判据同上面渲染那步,
145
+ # 不许各判各的):
146
+ # 有 s.yaml.domain.tmpl(app-builder):
147
+ # APP_DOMAIN_SUFFIX 非空 → 自定义域名,从 custom-domains 精确匹配
148
+ # APP_DOMAIN_SUFFIX 为空 → 使用者自带 key,FC 默认域名,从 httpTrigger 取
149
+ # 没有(persona-dataclaw-builder 等):自定义域名段写死在它自己的 s.yaml.tmpl 里,
150
+ # **原样走老路**——它的 functionName 规则、域名策略都跟这套无关,别替它选分支。
151
+ #
152
+ # JSON 解析用 node 不用 python3:目标机器保证有 node(clawd 本身就是 node 跑的),不保证有
153
+ # python。而且原先 `2>/dev/null | python3 -c` 把 CLI 的 stderr 吞了再让 python 抛
154
+ # JSONDecodeError,`set -e` 下脚本直接崩在 traceback 上,真凶(凭证不对 / 超时)全丢——
155
+ # 下面的 fc_get 保留 CLI 原文并 fail-loud。
156
+ fc_get() { # fc_get <path>:调 FC OpenAPI;失败把 CLI 原文写 stderr 后 return 1
157
+ local out rc
158
+ out="$(aliyun_ak fc GET "$1" 2>&1)" && rc=0 || rc=$?
159
+ if [ "$rc" -ne 0 ]; then
160
+ echo "❌ aliyun fc GET $1 失败,CLI 原文:" >&2
161
+ echo "$out" >&2
162
+ return 1
163
+ fi
164
+ printf '%s' "$out"
165
+ }
166
+ match_custom_domain() { # match_custom_domain <expected>:命中返回该域名,否则空
167
+ fc_get /2023-03-30/custom-domains | node -e '
168
+ const d = JSON.parse(require("fs").readFileSync(0, "utf8"));
169
+ const target = process.argv[1];
170
+ console.log((d.customDomains || []).some((c) => c.domainName === target) ? target : "");
171
+ ' "$1"
172
+ }
173
+
174
+ PROD_URL=""
175
+ ALLOW_CD=""
176
+ if [ -f "$DOMAIN_TMPL" ] && [ -z "${APP_DOMAIN_SUFFIX:-}" ]; then
177
+ # 单条 GetTrigger 而不是集合 ListTriggers:triggerName 由 persona 的 s.yaml.tmpl 契约固定
178
+ # 为 httpTrigger(写死是确定的,不是猜的)。
179
+ PROD_URL="$(fc_get "/2023-03-30/functions/${DEPLOY_NAME}/triggers/httpTrigger" | node -e '
180
+ const d = JSON.parse(require("fs").readFileSync(0, "utf8"));
181
+ console.log((d.httpTrigger && d.httpTrigger.urlInternet) || "");
182
+ ')"
183
+ MISS_HINT="未取到 FC 默认域名(函数 ${DEPLOY_NAME} 的 httpTrigger urlInternet 为空),检查 s deploy 是否真的建出了 httpTrigger"
184
+ # FC 默认域名(*.fcapp.run)对所有响应强制注入 Content-Disposition: attachment,没有
185
+ # 自定义域名就消不掉。使用者自带 key 履约的验收口径本来就是「公网可访问即可」
186
+ # (design ③:per-user 部署用 FC 默认域名,不要求品牌域名),所以降级成警告不判失败。
187
+ ALLOW_CD="--allow-content-disposition"
188
+ else
189
+ EXPECTED_DOM="${DEPLOY_NAME}.${APP_DOMAIN_SUFFIX:-app.clawos.chat}"
190
+ DOM="$(match_custom_domain "$EXPECTED_DOM")"
191
+ [ -n "$DOM" ] && PROD_URL="https://$DOM"
192
+ MISS_HINT="未取到自定义域名 $EXPECTED_DOM,检查 fc3-domain 是否成功 + DNS 泛解析 CNAME 是否生效"
193
+ fi
194
+
195
+ echo ""
196
+ echo "=================================================="
197
+ if [ -n "$PROD_URL" ]; then
198
+ echo "::prod-url::$PROD_URL"
199
+ echo " ✅ 发布完成: $PROD_URL"
200
+ echo "=================================================="
201
+ bash "$KIT_DIR/scripts/verify.sh" "$PROD_URL" $ALLOW_CD
202
+ else
203
+ echo "❌ 部署完成但$MISS_HINT" >&2
204
+ exit 1
205
+ fi