@clawos-dev/clawd 0.2.619 → 0.2.621
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/app-builder-plugin/mcp-server.cjs +79 -582
- package/dist/cli.cjs +313 -1720
- package/dist/dispatch/mcp-server.cjs +79 -595
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +6 -15
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +1 -2
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +3 -5
- package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/render-sql.mjs +4 -6
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +1 -7
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +3 -9
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +3 -9
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/table-name.ts +2 -5
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +1 -8
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +10 -47
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-toolchain.sh +3 -17
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/new-extension.sh +8 -21
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/publish.sh +14 -67
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/remove-extension.sh +6 -24
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/verify.sh +3 -15
- package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/mcp-server.cjs +79 -582
- package/dist/persona-defaults/persona-clawd-butler/.claude/skills/clawd-session-import/scripts/link-sessions.mjs +7 -23
- package/dist/persona-defaults/persona-clawd-butler/.claude/skills/scan-sessions-suggest-personas/scripts/extract-signals.mjs +6 -14
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +6 -15
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +1 -2
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +3 -5
- package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/render-sql.mjs +4 -6
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +2 -6
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.client.ts +2 -4
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.controller.ts +1 -2
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/main.ts +4 -11
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +1 -5
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/api.js +1 -4
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/BarChart.jsx +0 -1
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultCards.jsx +0 -1
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultTable.jsx +0 -1
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/enums.js +2 -3
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/hooks/pre-deploy.sh +0 -2
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +10 -47
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-toolchain.sh +3 -17
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/new-extension.sh +8 -21
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/publish.sh +14 -67
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/remove-extension.sh +6 -24
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/verify.sh +3 -15
- package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/mcp-server.cjs +79 -582
- package/dist/pi-mcp-bridge/extension.cjs +0 -1
- package/package.json +1 -1
|
@@ -41,7 +41,6 @@ function die(msg) {
|
|
|
41
41
|
process.exit(1)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// ---------------------------------------------------------------- 小工具
|
|
45
44
|
|
|
46
45
|
function clip(text, n) {
|
|
47
46
|
const s = String(text ?? '').split(/\s+/).filter(Boolean).join(' ')
|
|
@@ -121,7 +120,6 @@ function* jsonLines(file) {
|
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
|
|
124
|
-
// ---------------------------------------------------------------- RPC
|
|
125
123
|
|
|
126
124
|
function rpcConfig() {
|
|
127
125
|
try {
|
|
@@ -164,9 +162,7 @@ async function rpc(method, args = {}) {
|
|
|
164
162
|
return body
|
|
165
163
|
}
|
|
166
164
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
// 真实用户输入的文本;tool_result / 图片等块返回空串
|
|
165
|
+
// 真实用户文本;tool_result / 图片等块返回空串。
|
|
170
166
|
function userText(msg) {
|
|
171
167
|
const content = msg && typeof msg === 'object' ? msg.content : null
|
|
172
168
|
if (typeof content === 'string') return content
|
|
@@ -176,7 +172,6 @@ function userText(msg) {
|
|
|
176
172
|
return ''
|
|
177
173
|
}
|
|
178
174
|
|
|
179
|
-
// ~/.claude/projects/<编码后的 cwd>/<toolSessionId>.jsonl —— 文件名即 CC session id
|
|
180
175
|
function scanClaude(cutoffMs) {
|
|
181
176
|
const out = []
|
|
182
177
|
let dirs
|
|
@@ -210,7 +205,7 @@ function scanClaude(cutoffMs) {
|
|
|
210
205
|
let turns = 0
|
|
211
206
|
let firstTs = 0
|
|
212
207
|
let lastTs = 0
|
|
213
|
-
let hasMainLine = false
|
|
208
|
+
let hasMainLine = false
|
|
214
209
|
|
|
215
210
|
for (const rec of jsonLines(file)) {
|
|
216
211
|
if (!cwd && typeof rec.cwd === 'string') cwd = rec.cwd
|
|
@@ -222,7 +217,7 @@ function scanClaude(cutoffMs) {
|
|
|
222
217
|
}
|
|
223
218
|
}
|
|
224
219
|
if (rec.type === 'ai-title' && rec.aiTitle) {
|
|
225
|
-
title = rec.aiTitle
|
|
220
|
+
title = rec.aiTitle
|
|
226
221
|
continue
|
|
227
222
|
}
|
|
228
223
|
if (rec.type !== 'user' && rec.type !== 'assistant') continue
|
|
@@ -238,7 +233,6 @@ function scanClaude(cutoffMs) {
|
|
|
238
233
|
tool: 'claude',
|
|
239
234
|
toolSessionId: name.slice(0, -'.jsonl'.length),
|
|
240
235
|
cwd,
|
|
241
|
-
// 优先 CC 生成的 ai-title,没有就拿第一句用户发言硬截
|
|
242
236
|
label: clipW(title || firstUser, 56),
|
|
243
237
|
turns,
|
|
244
238
|
createdMs: firstTs || st.mtimeMs,
|
|
@@ -255,9 +249,7 @@ function scanClaude(cutoffMs) {
|
|
|
255
249
|
return out
|
|
256
250
|
}
|
|
257
251
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
// 从 rollout jsonl 数真实用户轮次,顺带取第一条用户发言
|
|
252
|
+
// 从 rollout jsonl 统计用户轮次并取第一条用户文本。
|
|
261
253
|
function countCodexTurns(rolloutPath) {
|
|
262
254
|
if (!rolloutPath || !fs.existsSync(rolloutPath)) return { turns: 0, first: '' }
|
|
263
255
|
let turns = 0
|
|
@@ -334,7 +326,7 @@ function codexFromRows(rows, cutoffMs) {
|
|
|
334
326
|
const updatedMs = toMs(r.updated_at)
|
|
335
327
|
if (updatedMs < cutoffMs) continue
|
|
336
328
|
const { turns, first } = countCodexTurns(r.rollout_path || '')
|
|
337
|
-
//
|
|
329
|
+
// 标签可能是用户原话,按显示宽度截断。
|
|
338
330
|
const label = clipW(r.name || r.title || r.preview || r.first_user_message || first, 56)
|
|
339
331
|
const cand = {
|
|
340
332
|
tool: 'codex',
|
|
@@ -394,7 +386,6 @@ function scanCodexRollouts(cutoffMs) {
|
|
|
394
386
|
return out
|
|
395
387
|
}
|
|
396
388
|
|
|
397
|
-
// ---------------------------------------------------------------- 过滤 / 关联
|
|
398
389
|
|
|
399
390
|
async function existingToolSessionIds() {
|
|
400
391
|
const res = await rpc('session:list', { limit: 100000 })
|
|
@@ -457,7 +448,6 @@ async function link(cand, icon, ownerPersonaId = '') {
|
|
|
457
448
|
return { sessionId, linked: !imported.alreadyLinked }
|
|
458
449
|
}
|
|
459
450
|
|
|
460
|
-
// ---------------------------------------------------------------- 台账 / 撤销
|
|
461
451
|
|
|
462
452
|
function ledgerRead() {
|
|
463
453
|
try {
|
|
@@ -485,9 +475,7 @@ async function doUndo(applyIt) {
|
|
|
485
475
|
const res = await rpc('session:list', { limit: 100000 })
|
|
486
476
|
const alive = new Set((res.sessions || []).map((s) => s.sessionId))
|
|
487
477
|
const todo = entries.filter((e) => alive.has(e.sessionId))
|
|
488
|
-
//
|
|
489
|
-
// 把账清掉。扫盘去重会读这本台账(spec 2026-08-19 §4),条目留着就等于该会话永久
|
|
490
|
-
// 无法重新导入,而 undo 的语义本就是「撤销我的导入记录」。
|
|
478
|
+
// 不存在的 session 也需清台账,否则去重会阻止重新导入;删除失败项保留供重试。
|
|
491
479
|
const stale = entries.filter((e) => !alive.has(e.sessionId))
|
|
492
480
|
console.log(`台账 ${entries.length} 条,其中 ${todo.length} 条还在` +
|
|
493
481
|
(stale.length ? `,${stale.length} 条已不存在(只清账)` : '') + '。' +
|
|
@@ -515,10 +503,7 @@ async function doUndo(applyIt) {
|
|
|
515
503
|
console.log(`已删除 ${removed} 条,清账 ${doneIds.size} 条。原 Claude Code / Codex 的会话文件没有任何改动。`)
|
|
516
504
|
}
|
|
517
505
|
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
// clawd UI 认得的图标 key(源:ui/src/lib/session-icons.ts)。协议侧 iconKey 是自由字符串,
|
|
521
|
-
// daemon 不拒绝拼错的值,UI 查不到就不画图标——零提示,所以在参数层拦。
|
|
506
|
+
// iconKey 协议允许任意字符串,但 UI 未识别就不显示;在参数层校验已知值。
|
|
522
507
|
const ICON_KEYS = ['research', 'code', 'loop', 'qa', 'reading', 'debug', 'idea', 'doc', 'assist']
|
|
523
508
|
|
|
524
509
|
function parseArgs(argv) {
|
|
@@ -573,7 +558,6 @@ const HELP = `把本机 Claude Code / Codex 历史会话关联进 clawd
|
|
|
573
558
|
--undo 撤销本工具关联过的会话(配 --apply 才真删)
|
|
574
559
|
--json 输出 JSON(含被跳过的和原因)`
|
|
575
560
|
|
|
576
|
-
// ---------------------------------------------------------------- main
|
|
577
561
|
|
|
578
562
|
async function main() {
|
|
579
563
|
const args = parseArgs(process.argv.slice(2))
|
|
@@ -26,7 +26,7 @@ const CLAUDE_PROJECTS = path.join(HOME, '.claude', 'projects');
|
|
|
26
26
|
const CODEX_DIR = path.join(HOME, '.codex');
|
|
27
27
|
const PERSONAS_DIR = path.join(HOME, '.clawd', 'personas');
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// 系统注入文本不计用户需求。
|
|
30
30
|
const DROP_PREFIX = [
|
|
31
31
|
'<system-reminder', '<command-name', '<command-message', '<command-args',
|
|
32
32
|
'<local-command-stdout', '<local-command-stderr', '<user-prompt-submit-hook',
|
|
@@ -42,8 +42,7 @@ const B64 = /data:image\/[a-z]+;base64,[A-Za-z0-9+/=]+/g;
|
|
|
42
42
|
|
|
43
43
|
const clean = (t) => t.replace(B64, '[图片]').replace(/[ \t]+/g, ' ').trim();
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* 交给下游聚类稀释——占比极低,不值得为它引入一层判断。 */
|
|
45
|
+
/** 只滤结构可识别的噪音,语义噪音交给下游聚类。 */
|
|
47
46
|
function isRealQuery(t) {
|
|
48
47
|
if (!t || t.length < 6) return false;
|
|
49
48
|
if (DROP_PREFIX.some((p) => t.startsWith(p))) return false;
|
|
@@ -71,7 +70,6 @@ function textOf(content) {
|
|
|
71
70
|
return parts.join(' ');
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
// ---------- 小工具 ----------
|
|
75
73
|
const bump = (m, k, n = 1) => m.set(k, (m.get(k) || 0) + n);
|
|
76
74
|
// 计数相同时按名字排——否则并列项谁进前 N 随遍历顺序漂移,同样输入得不到同样输出
|
|
77
75
|
const topN = (m, n) =>
|
|
@@ -123,7 +121,7 @@ async function eachLine(file, fn) {
|
|
|
123
121
|
}
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
//
|
|
124
|
+
// 已有 persona 覆盖的目录一律跳过,无开关。
|
|
127
125
|
function knownPersonaDirs() {
|
|
128
126
|
const out = new Map();
|
|
129
127
|
let ents;
|
|
@@ -167,7 +165,6 @@ const profOf = (prof, k) => {
|
|
|
167
165
|
return prof.get(k);
|
|
168
166
|
};
|
|
169
167
|
|
|
170
|
-
// ---------- Claude Code ----------
|
|
171
168
|
async function scanClaude(cutMs, sig, prof) {
|
|
172
169
|
let dirs;
|
|
173
170
|
try {
|
|
@@ -200,8 +197,7 @@ async function scanClaude(cutMs, sig, prof) {
|
|
|
200
197
|
const typ = d.type;
|
|
201
198
|
const cwd = d.cwd || '';
|
|
202
199
|
|
|
203
|
-
//
|
|
204
|
-
// 比任何启发式取样都准,是画像的首选信号(约 9 成会话有)
|
|
200
|
+
// CC 生成的 ai-title 优先作为任务摘要信号。
|
|
205
201
|
if (typ === 'ai-title' && d.aiTitle) {
|
|
206
202
|
p.titles.push(d.aiTitle);
|
|
207
203
|
return;
|
|
@@ -234,9 +230,7 @@ async function scanClaude(cutMs, sig, prof) {
|
|
|
234
230
|
}
|
|
235
231
|
}
|
|
236
232
|
|
|
237
|
-
|
|
238
|
-
/** threads 表是 codex 的权威索引,比解析 rollout 准,还带 git/model 信息。
|
|
239
|
-
* node:sqlite 要 node>=22.5 且是实验特性——拿不到就返回空,由 rollout 扫描兜底。 */
|
|
233
|
+
/** 可用时从 Codex threads 表取 cwd / branch;旧 Node 或缺数据库时仍扫描 rollout。 */
|
|
240
234
|
async function codexMetaFromDb() {
|
|
241
235
|
const out = new Map();
|
|
242
236
|
let dbs;
|
|
@@ -331,7 +325,6 @@ async function scanCodex(cutMs, sig, prof) {
|
|
|
331
325
|
}
|
|
332
326
|
}
|
|
333
327
|
|
|
334
|
-
// ---------- 主流程 ----------
|
|
335
328
|
function parseArgs(argv) {
|
|
336
329
|
const a = { days: 7, out: '.', samples: 12, minQueries: 5, minDays: 2 };
|
|
337
330
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -393,8 +386,7 @@ async function main() {
|
|
|
393
386
|
const qs = byProj.get(k);
|
|
394
387
|
if (!qs || !qs.length) continue;
|
|
395
388
|
|
|
396
|
-
//
|
|
397
|
-
// 这里的活已经有人干了。不给开关——「要不要看已覆盖目录」不是每次运行要重想的问题。
|
|
389
|
+
// 已覆盖目录不产生候选,不提供开关,避免把既有 persona 的工作再次推荐。
|
|
398
390
|
const owner = coveredBy(v.cwd, known);
|
|
399
391
|
if (owner) {
|
|
400
392
|
skipped.push([k, owner, qs.length]);
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// + 永远排掉 `.git` 和 `.clawd-project.json`(插件的记账文件:publish / record-push 都会改它,进了仓库或哈希就会
|
|
6
|
-
// 「每推一次都变一次」,而且里面的 deployments / pushedCommit 也不是源码)。**不要**在这里另写一份排除表:publish.sh 渲进项目目录的 s.int.yaml / s.prod.yaml 含 supabase key,
|
|
7
|
-
// 它进不进仓库只该由 .gitignore 一处决定。
|
|
8
|
-
// 二进制(前 8KB 含 NUL)和超过 512KB 的文件跳过并列进 `skipped`——push_files 只收文本,
|
|
9
|
-
// 而且每个字节都要经模型上下文走一遍;agent 看到 skipped 里有该进仓库的东西要自己判断。
|
|
10
|
-
//
|
|
2
|
+
// 收集 GitHub MCP push_files 的文本文件;过滤以项目 .gitignore 为准,避免 s.*.yaml 中的 key 被提交。
|
|
3
|
+
// 始终排除 .git 及根 .clawd-project.json,记账变化不属于源码,不能进入推送或树哈希。
|
|
4
|
+
// 二进制(前 8KB 含 NUL)及超过 512KB 的文件列入 skipped,不进哈希;调用方需判断是否另行处理。
|
|
11
5
|
// 用法:collect-files.mjs --project <项目目录绝对路径>
|
|
12
6
|
// 输出:{ files: [{ path, content }], skipped: [{ path, reason }], treeHash }
|
|
13
|
-
//
|
|
14
|
-
// publish tool 再跑一次本脚本比对——不一致就是「改了没推」,拒绝发布。skipped 里的文件不进哈希。
|
|
7
|
+
// treeHash 为按 path 排序的 sha256(path\0content\0…);publish 重算后与 record-push 记账比较,拒绝未推的改动。
|
|
15
8
|
import { createHash } from 'node:crypto'
|
|
16
9
|
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
17
10
|
import { join, relative, sep } from 'node:path'
|
|
@@ -58,7 +51,6 @@ function ignored(rel, isDir, rules) {
|
|
|
58
51
|
const segments = rel.split('/')
|
|
59
52
|
for (const rule of rules) {
|
|
60
53
|
if (rule.anchored) {
|
|
61
|
-
// 前缀匹配:rel 等于它、或在它下面
|
|
62
54
|
for (let i = 1; i <= segments.length; i++) {
|
|
63
55
|
const head = segments.slice(0, i).join('/')
|
|
64
56
|
if (rule.re.test(head) && (!rule.dirOnly || isDir || i < segments.length)) return true
|
|
@@ -67,7 +59,6 @@ function ignored(rel, isDir, rules) {
|
|
|
67
59
|
}
|
|
68
60
|
for (let i = 0; i < segments.length; i++) {
|
|
69
61
|
if (!rule.re.test(segments[i])) continue
|
|
70
|
-
// dirOnly:命中的这一段得是目录——要么它不是最后一段(后面还有东西),要么 rel 本身就是目录
|
|
71
62
|
if (!rule.dirOnly || i < segments.length - 1 || isDir) return true
|
|
72
63
|
}
|
|
73
64
|
}
|
|
@@ -78,7 +69,7 @@ let rules = []
|
|
|
78
69
|
try {
|
|
79
70
|
rules = parseGitignore(await readFile(join(root, '.gitignore'), 'utf8'))
|
|
80
71
|
} catch {
|
|
81
|
-
//
|
|
72
|
+
// 无法读取 .gitignore 时仍排除 .git 和根 .clawd-project.json。
|
|
82
73
|
}
|
|
83
74
|
|
|
84
75
|
const files = []
|
|
@@ -109,7 +100,7 @@ async function walk(dir) {
|
|
|
109
100
|
}
|
|
110
101
|
|
|
111
102
|
await walk(root)
|
|
112
|
-
//
|
|
103
|
+
// 按字符串序而非 localeCompare 排序,避免不同机器 locale 改变树哈希。
|
|
113
104
|
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
114
105
|
const hash = createHash('sha256')
|
|
115
106
|
for (const f of files) hash.update(f.path).update('\0').update(f.content).update('\0')
|
|
@@ -24,8 +24,7 @@ for (const dirName of names) {
|
|
|
24
24
|
try {
|
|
25
25
|
const value = JSON.parse(await readFile(join(projectsRoot, dirName, '.clawd-project.json'), 'utf8'))
|
|
26
26
|
if (value?.name !== dirName || typeof value.createdAt !== 'string') continue
|
|
27
|
-
//
|
|
28
|
-
// 插件还没读写过的老文件顶层就有 prodUrl,原样带出。
|
|
27
|
+
// prodUrl 兼容字段优先取 deployments.prod.url,缺席时保留 legacy 顶层值。
|
|
29
28
|
const prodUrl = value.deployments?.prod?.url
|
|
30
29
|
projects.push({ ...value, ...(prodUrl ? { prodUrl } : {}), projectDir: join(projectsRoot, dirName) })
|
|
31
30
|
} catch {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// 用法:record-push.mjs --project <项目目录绝对路径> --commit <push_files 返回的 object.sha> --tree-hash <collect-files.mjs 输出的 treeHash> [--repo-url <https://github.com/ottin4ttc/...>]
|
|
2
|
+
// GitHub MCP push 后记 repoUrl、pushedCommit 和 pushedTreeHash 到 .clawd-project.json。
|
|
3
|
+
// 无本地 git 的项目发布时重算树哈希;一致才以 pushedCommit 记 publishedCommit。
|
|
4
|
+
// 用法:record-push.mjs --project <项目目录绝对路径> --commit <object.sha> --tree-hash <treeHash> [--repo-url <https://github.com/ottin4ttc/...>]
|
|
7
5
|
// 输出:写回后的元数据 JSON
|
|
8
6
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
9
7
|
import { join } from 'node:path'
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// integration → <APP_NAME>_<SLUG>_int_ prod → <APP_NAME>_<SLUG>_
|
|
6
|
-
// ext.conf 没 SLUG 的老项目退成 <APP_NAME>_int_ / <APP_NAME>_。
|
|
2
|
+
// 渲 server/db/schema.sql 的 __TABLE_PREFIX__ 到 stdout,共用一份 DDL。
|
|
3
|
+
// 前缀与 publish/remove 一致:integration 为 <APP_NAME>_<SLUG>_int_,prod 为 <APP_NAME>_<SLUG>_。
|
|
4
|
+
// legacy 无 SLUG 时分别退为 <APP_NAME>_int_ / <APP_NAME>_。
|
|
7
5
|
import { readFile } from 'node:fs/promises'
|
|
8
6
|
import { join } from 'node:path'
|
|
9
7
|
|
|
@@ -27,7 +25,7 @@ function fail(msg) {
|
|
|
27
25
|
process.exit(1)
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
/**
|
|
28
|
+
/** 只解析单行 KEY=value 及单/双引号,不执行 ext.conf 的 shell 内容。 */
|
|
31
29
|
function readConf(text) {
|
|
32
30
|
const out = {}
|
|
33
31
|
for (const line of text.split('\n')) {
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { Module } from '@nestjs/common';
|
|
2
2
|
import { DataclawModule } from './dataclaw/dataclaw.module';
|
|
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
|
+
// 起手注册 DataclawModule 的五个只读端点;apikey 只由后端以 Bearer 发往 dataclaw。
|
|
5
|
+
// 静态资源由 main.ts 托管,无需 ServeStaticModule。
|
|
10
6
|
@Module({
|
|
11
7
|
imports: [DataclawModule],
|
|
12
8
|
})
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Injectable, HttpException, OnModuleInit } from '@nestjs/common'
|
|
2
2
|
import { DataclawConfig, loadDataclawConfig } from './dataclaw.config'
|
|
3
3
|
|
|
4
|
-
// dataclaw
|
|
5
|
-
// 本 app 唯一出站目标就是 dataclaw 这一个自签 https,进程级放开 TLS 校验即可(最小改动、零依赖)。
|
|
4
|
+
// dataclaw 服务使用自签证书;这里关闭整个进程的 TLS 证书验证,会影响所有出站 HTTPS。
|
|
6
5
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
7
6
|
|
|
8
7
|
// 端点超时:schema / 固定查询走默认;intent-execute 可能跑 LLM 编排,给 90s。
|
|
@@ -50,8 +49,7 @@ export class DataclawClient implements OnModuleInit {
|
|
|
50
49
|
throw new HttpException({ ok: false, error: `dataclaw 非 JSON 响应: ${text.slice(0, 200)}` }, 502)
|
|
51
50
|
}
|
|
52
51
|
if (!res.ok || json?.ok === false) {
|
|
53
|
-
//
|
|
54
|
-
// (别把 200 当错误码抛——对前端是"成功码带错误体"的矛盾态)。
|
|
52
|
+
// 2xx 的应用层 ok:false 及上游 5xx 统一映射为 502,避免以成功状态返回错误体。
|
|
55
53
|
const status = res.ok ? 502 : res.status >= 500 ? 502 : res.status || 400
|
|
56
54
|
throw new HttpException(
|
|
57
55
|
{ ok: false, error: json?.error || `dataclaw ${res.status}`, upstreamStatus: res.status },
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Body, Controller, Get, Post, Query } from '@nestjs/common'
|
|
2
2
|
import { DataclawClient } from './dataclaw.client'
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
// 故这里的路径不带 /api。前端只调本 app 的 /api/*,apikey 由后端注入,绝不下发浏览器。
|
|
4
|
+
// main.ts 统一添加 /api 前缀;路由不重复写它。apikey 只在后端,不下发浏览器。
|
|
6
5
|
@Controller()
|
|
7
6
|
export class DataclawController {
|
|
8
7
|
constructor(private readonly dc: DataclawClient) {}
|
|
@@ -5,11 +5,8 @@ import { NestExpressApplication } from '@nestjs/platform-express';
|
|
|
5
5
|
import { AppModule } from './app.module';
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// /api 走 web/vite.config.js 的 proxy 转到本进程)
|
|
11
|
-
// - prod / FC:nest 直接 serve web 的打包产物(publish.sh 的 BUILD_CMD 把 web/dist 拷进 dist/public)
|
|
12
|
-
// FC_SERVER_PORT 是 FC 运行时注入的端口;PORT 给本地调试。
|
|
8
|
+
// dev 由 Vite 独跑并代理 /api;FC 托管拷入 dist/public 的 web 产物。
|
|
9
|
+
// 端口优先 FC_SERVER_PORT,其次本地 PORT。
|
|
13
10
|
async function bootstrap() {
|
|
14
11
|
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
|
15
12
|
|
|
@@ -18,13 +15,9 @@ async function bootstrap() {
|
|
|
18
15
|
|
|
19
16
|
app.setGlobalPrefix('api', { exclude: ['/'] });
|
|
20
17
|
|
|
21
|
-
//
|
|
22
|
-
// 见 dataclaw.client —— key 绝不下发浏览器。
|
|
18
|
+
// 页面无登录门禁,apikey 仅由后端使用。
|
|
23
19
|
|
|
24
|
-
//
|
|
25
|
-
// 1. FC(CODE_DIR=./server 只上传 server/,运行时 __dirname=dist/):publish.sh BUILD_CMD 末尾
|
|
26
|
-
// 把 ../web/dist 拷进 dist/public,跟着 server 一起上传,所以优先用 __dirname/public
|
|
27
|
-
// 2. 本地 `node dist/main.js`:__dirname/public 不存在时 fallback 到 ../../web/dist
|
|
20
|
+
// FC 仅上传 server/,优先托管 dist/public;本地构建可回落到 web/dist。
|
|
28
21
|
const fs = await import('node:fs');
|
|
29
22
|
const fcStaticDir = path.resolve(__dirname, 'public');
|
|
30
23
|
const monorepoStaticDir = path.resolve(__dirname, '..', '..', 'web', 'dist');
|
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
// server 拿不到 DATACLAW_* env → loadDataclawConfig 会 fail-loud。
|
|
3
|
-
// 这里在 nest bootstrap 之前显式 require dotenv 把 .env 填进 process.env。
|
|
4
|
-
// prod 路径(node --env-file=.env dist/main.js)Node 自带 loader 已经填了,dotenv 看到
|
|
5
|
-
// 已存在的 env 不会覆盖(默认行为),行为对齐。
|
|
1
|
+
// nest watch 不经过 Node --env-file;bootstrap 前加载 .env,dotenv 不覆盖已注入的 env。
|
|
6
2
|
require('dotenv/config');
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// dev 隧道模式下页面挂在子路径 /preview/<port>/ 下(见 vite.config.js base),
|
|
4
|
-
// 所以 API_BASE 要拼上 import.meta.env.BASE_URL,否则 fetch 会打到错误的绝对路径。
|
|
1
|
+
// 只调本 app 的 BFF,apikey 留在后端;API 路径相对 Vite BASE_URL(默认 /)。
|
|
5
2
|
const API_BASE = (import.meta.env.BASE_URL || '/').replace(/\/+$/, '') + '/api';
|
|
6
3
|
|
|
7
4
|
async function req(path, opts = {}) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { INTERNAL_FIELDS } from '../enums';
|
|
2
2
|
|
|
3
|
-
// 结果表格。列取 rows[0] 的 key(过滤 _provenance 等内部字段),数字按 zh-CN 千分位展示,空数组显示「无数据」。
|
|
4
3
|
export default function ResultTable({ rows }) {
|
|
5
4
|
if (!rows || rows.length === 0) return <div className="empty">无数据</div>;
|
|
6
5
|
const cols = Object.keys(rows[0]).filter((c) => !INTERNAL_FIELDS.has(c));
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
// ResultTable 渲染时按此 Set 过滤掉对应列。
|
|
1
|
+
// 渲染结果时隐藏内部编号、ID 及溯源元数据。
|
|
3
2
|
export const INTERNAL_FIELDS = new Set([
|
|
4
3
|
'ID', 'unique_id', '链接', 'SourceID', '发票ID', '客户ID', '职位ID',
|
|
5
4
|
'业绩所属一级团队编号', '业绩所属二级团队编号', '业绩所属三级团队编号', '所属OP名称', '候选人ID',
|
|
6
|
-
'_provenance',
|
|
5
|
+
'_provenance',
|
|
7
6
|
]);
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# dataclaw 发布前置:从生成 app 的 server/.env 读出建页人填的 apikey,export 供 s.yaml 占位符替换。
|
|
3
3
|
# 由 deploy-kit/publish.sh 在渲染 s.yaml 前 source(共享 EXT_DIR/APP_NAME/PERSONA_KIT 等作用域)。
|
|
4
4
|
|
|
5
|
-
# 建页人提供的 dataclaw apikey 写在生成 app 的 server/.env(DATACLAW_API_KEY)。
|
|
6
5
|
# .env 被 server/.fcignore 排除、不进部署包,所以这里读出来、经 s.yaml 注入 FC 环境变量。
|
|
7
6
|
ENV_FILE="$EXT_DIR/server/.env"
|
|
8
7
|
if [ -f "$ENV_FILE" ]; then
|
|
@@ -17,5 +16,4 @@ if [ -z "${DATACLAW_API_KEY:-}" ]; then
|
|
|
17
16
|
fi
|
|
18
17
|
export DATACLAW_API_KEY
|
|
19
18
|
|
|
20
|
-
# service url:config.env / ext.conf 已 source 进来;兜底公网地址。
|
|
21
19
|
export DATACLAW_SERVICE_URL="${DATACLAW_SERVICE_URL:-https://120.48.21.36}"
|
|
@@ -1,30 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
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
|
-
# ============================================================
|
|
2
|
+
# Source this file and call ensure_credentials before publish/remove.
|
|
3
|
+
# Credentials stay in process env; never overwrite the user global Serverless Devs credentials.
|
|
14
4
|
|
|
15
|
-
#
|
|
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)照样走这条。
|
|
5
|
+
# Use the literal environment access alias so missing env cannot silently fall back to the user profile.
|
|
19
6
|
S_ACCESS_ALIAS='$system_environment_access'
|
|
20
7
|
|
|
21
|
-
#
|
|
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。
|
|
8
|
+
# Require caller-injected AK values; never ship credentials or read fallback kit credential files.
|
|
28
9
|
ensure_aliyun_env() {
|
|
29
10
|
if [ -z "${ALIBABA_CLOUD_ACCESS_KEY_ID:-}" ] || [ -z "${ALIBABA_CLOUD_ACCESS_KEY_SECRET:-}" ]; then
|
|
30
11
|
echo "❌ 没有阿里云 AK:本机把 ALIBABA_CLOUD_ACCESS_KEY_ID / ALIBABA_CLOUD_ACCESS_KEY_SECRET 两行写进 ~/.clawd/secrets/aliyun.env(daemon 起 clawd-app-builder 时渲进它的 env);云上由 gateway 注入。" >&2
|
|
@@ -33,17 +14,7 @@ ensure_aliyun_env() {
|
|
|
33
14
|
export ALIBABA_CLOUD_REGION_ID="${REGION:?config.env 缺 REGION}"
|
|
34
15
|
}
|
|
35
16
|
|
|
36
|
-
#
|
|
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
|
|
17
|
+
# Pass AK and region explicitly: aliyun AK mode can otherwise fall back to the user profile.
|
|
47
18
|
aliyun_ak() {
|
|
48
19
|
aliyun --mode AK \
|
|
49
20
|
--access-key-id "${ALIBABA_CLOUD_ACCESS_KEY_ID:?aliyun_ak 需要先 ensure_aliyun_env}" \
|
|
@@ -52,41 +23,33 @@ aliyun_ak() {
|
|
|
52
23
|
"$@"
|
|
53
24
|
}
|
|
54
25
|
|
|
55
|
-
#
|
|
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 重装一次。
|
|
26
|
+
# Derive AccountID through STS and isolate Serverless Devs cache/config under KIT_DIR; do not write credentials.
|
|
62
27
|
ensure_s_access() {
|
|
63
28
|
local kit="${KIT_DIR:?ensure_s_access 需要调用方先设 KIT_DIR}"
|
|
64
29
|
local s_home="$kit/.s-home"
|
|
65
30
|
local ident acc
|
|
66
31
|
|
|
67
32
|
ident="$(aliyun_ak sts GetCallerIdentity 2>&1)" || {
|
|
68
|
-
#
|
|
69
|
-
# 把 CLI 原文摆出来,让读的人按原文判断。
|
|
33
|
+
# Preserve CLI diagnostics: identity failures are not necessarily bad credentials.
|
|
70
34
|
echo "❌ aliyun sts GetCallerIdentity 失败,拿不到 AccountId。aliyun 原文:" >&2
|
|
71
35
|
echo "$ident" >&2
|
|
72
36
|
echo " 排查顺序:① 上面这行 CLI 原文说的是什么 ② 进程 env 里的 AK 是否有效(本机 ~/.clawd/secrets/aliyun.env、云上仓库 Secrets)——2026-09-18 之前 OTA 发的那把已泄露、不可再用,得换自己的 ③ config.env 的 REGION" >&2
|
|
73
37
|
return 1
|
|
74
38
|
}
|
|
75
|
-
#
|
|
76
|
-
# —— 清理路径尤其不该多一个装不上就跑不了的依赖。
|
|
39
|
+
# Node is guaranteed by clawd; do not add a Python runtime prerequisite.
|
|
77
40
|
acc="$(printf '%s' "$ident" | node -e 'console.log(JSON.parse(require("fs").readFileSync(0,"utf8")).AccountId)')" || {
|
|
78
41
|
echo "❌ 解析 AccountId 失败,aliyun 输出非 JSON:" >&2
|
|
79
42
|
echo "$ident" >&2
|
|
80
43
|
return 1
|
|
81
44
|
}
|
|
82
45
|
|
|
83
|
-
#
|
|
46
|
+
# Serverless Devs credential environment keys are case-sensitive.
|
|
84
47
|
export AccountID="$acc"
|
|
85
48
|
export AccessKeyID="$ALIBABA_CLOUD_ACCESS_KEY_ID"
|
|
86
49
|
export AccessKeySecret="$ALIBABA_CLOUD_ACCESS_KEY_SECRET"
|
|
87
50
|
export serverless_devs_config_home="$s_home"
|
|
88
51
|
mkdir -p "$s_home/.s"
|
|
89
|
-
#
|
|
52
|
+
# Remove legacy persisted credentials from the kit cache.
|
|
90
53
|
rm -f "$s_home/.s/access.yaml"
|
|
91
54
|
}
|
|
92
55
|
|
|
@@ -1,19 +1,7 @@
|
|
|
1
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
|
-
# ============================================================
|
|
2
|
+
# Source and call ensure_toolchain before credential setup; install missing CLI tools without configuring user profiles.
|
|
15
3
|
|
|
16
|
-
#
|
|
4
|
+
# Pinned fallback applies only to direct binary download, not Homebrew.
|
|
17
5
|
CLAWD_ALIYUN_CLI_VERSION="${CLAWD_ALIYUN_CLI_VERSION:-3.3.21}"
|
|
18
6
|
|
|
19
7
|
ensure_s() {
|
|
@@ -26,12 +14,10 @@ ensure_s() {
|
|
|
26
14
|
ensure_aliyun() {
|
|
27
15
|
command -v aliyun >/dev/null 2>&1 && return 0
|
|
28
16
|
echo "==> 安装阿里云 CLI (aliyun)…"
|
|
29
|
-
# 优先 brew(有 brew 的 mac 最省事,装到已在 PATH 的 /opt/homebrew/bin | /usr/local/bin)
|
|
30
17
|
if command -v brew >/dev/null 2>&1; then
|
|
31
18
|
brew install aliyun-cli
|
|
32
19
|
return 0
|
|
33
20
|
fi
|
|
34
|
-
# 兜底:官方 release 二进制(没装 brew 的新机器 / Linux server)。
|
|
35
21
|
local os arch bindir="$HOME/.clawd/bin"
|
|
36
22
|
case "$(uname -s)" in
|
|
37
23
|
Darwin) os=macosx ;;
|
|
@@ -57,7 +43,7 @@ ensure_toolchain() {
|
|
|
57
43
|
ensure_aliyun
|
|
58
44
|
}
|
|
59
45
|
|
|
60
|
-
#
|
|
46
|
+
# Direct execution checks the toolchain; sourcing leaves execution to the caller.
|
|
61
47
|
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
62
48
|
set -euo pipefail
|
|
63
49
|
ensure_toolchain
|