@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.
Files changed (45) hide show
  1. package/dist/app-builder-plugin/mcp-server.cjs +79 -582
  2. package/dist/cli.cjs +313 -1720
  3. package/dist/dispatch/mcp-server.cjs +79 -595
  4. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +6 -15
  5. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +1 -2
  6. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +3 -5
  7. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/render-sql.mjs +4 -6
  8. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +1 -7
  9. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +3 -9
  10. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +3 -9
  11. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/table-name.ts +2 -5
  12. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +1 -8
  13. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +10 -47
  14. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-toolchain.sh +3 -17
  15. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/new-extension.sh +8 -21
  16. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/publish.sh +14 -67
  17. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/remove-extension.sh +6 -24
  18. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/deploy-kit/scripts/verify.sh +3 -15
  19. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/mcp-server.cjs +79 -582
  20. package/dist/persona-defaults/persona-clawd-butler/.claude/skills/clawd-session-import/scripts/link-sessions.mjs +7 -23
  21. package/dist/persona-defaults/persona-clawd-butler/.claude/skills/scan-sessions-suggest-personas/scripts/extract-signals.mjs +6 -14
  22. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +6 -15
  23. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +1 -2
  24. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +3 -5
  25. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/render-sql.mjs +4 -6
  26. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +2 -6
  27. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.client.ts +2 -4
  28. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.controller.ts +1 -2
  29. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/main.ts +4 -11
  30. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +1 -5
  31. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/api.js +1 -4
  32. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/BarChart.jsx +0 -1
  33. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultCards.jsx +0 -1
  34. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultTable.jsx +0 -1
  35. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/enums.js +2 -3
  36. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/hooks/pre-deploy.sh +0 -2
  37. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-credentials.sh +10 -47
  38. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/ensure-toolchain.sh +3 -17
  39. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/new-extension.sh +8 -21
  40. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/publish.sh +14 -67
  41. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/remove-extension.sh +6 -24
  42. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/deploy-kit/scripts/verify.sh +3 -15
  43. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/mcp-server.cjs +79 -582
  44. package/dist/pi-mcp-bridge/extension.cjs +0 -1
  45. 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
- // ---------------------------------------------------------------- Claude Code 扫描
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 // 有非 sidechain 的正文行 = 不是 subagent 支线文件
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 // CC 自己生成的标题,最适合当 label
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
- // ---------------------------------------------------------------- Codex 扫描
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
- // codex 这几列全是用户原话(name 疑似手动命名字段,本机数据全空),硬截
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
- // 台账里 session 已经不在了(多半是用户直接在 clawd UI 里删的):没什么可删,只需
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
- /** 结构上能判定的噪音在这里滤干净。语义噪音(agent 自造的探针指令等)正则救不了,
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
- // ---------- 已有 persona:这些目录下的活已经有人干了,默认跳过 ----------
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
- // Claude Code 自己生成的会话标题:一句已抽象好的任务摘要,
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
- // ---------- Codex ----------
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
- // 已经有 persona 管着的目录整个略过:本脚本的意义是找**没人管的活**,
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
- // 把项目目录里该进仓库的文件收成 GitHub MCP `push_files` 要的 `[{ path, content }]`。
3
- //
4
- // 过滤规则 = 项目根 `.gitignore`(模板那份:node_modules/ dist/ .env .publish.log s.yaml s.*.yaml server/bootstrap .s/ …)
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
- // treeHash = files 按 path 排序后 sha256(path\0content\0…)。record-push.mjs 把它记进元数据,
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
- // 没有 .gitignore:只排 .git
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
- // 按字节序排,不用 localeCompare:不同机器 locale 不同会让同一棵树排出两种顺序
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
- // 双环境后发布记录在 deployments.{integration,prod};prodUrl 保留作兼容字段 = prod 那份的 url。
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
- // 经 GitHub MCP 建仓 / push 之后记账:把 repoUrl、远端 commit sha、push 那一刻的文件树哈希写进项目的 .clawd-project.json。
3
- // publish tool 对没有本地 git 的项目:重算文件树哈希与 pushedTreeHash 比,不一致拒绝发布;一致就记 pushedCommit
4
- // 当 publishedCommit(「线上 = 仓库里的哪个 commit」)。
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
- // 把项目的 server/db/schema.sql 渲成某个环境的 SQL 打到 stdout:`__TABLE_PREFIX__` → 该环境的表前缀。
3
- // 表结构只有这一份源,integration / prod 各渲一次再喂给 supabase MCP 的 execute_sql——两套表结构
4
- // 不会漂移。前缀口径与 publish.sh / remove-extension.sh 相同:
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
- /** ext.conf 是 bash 赋值文件;这里只认 `KEY=value` / `KEY="value"` 单行,够 APP_NAME / SLUG 用。 */
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
- // dataclaw 只读后端 BFF 起手骨架:默认只注册 DataclawModule(schema / performance / ppl /
5
- // consultants / intent 五个只读端点,用建页人提供的 apikey 以 Bearer 调公网 dataclaw service)。
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 公网 service 用自签证书(文档要求 curl -k)。Node fetch(undici) 默认校验 TLS 会握手失败。
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
- // HTTP 非 2xx → 映射上游状态(5xx 归 502);HTTP 2xx 但应用层 {ok:false} → 502
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
- // app 自己的只读 BFF。main.ts 已 setGlobalPrefix('api' 或子路径 'preview/<port>/api'),
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
- // - dev:`cd server && pnpm dev`(nest watch)+ 另开一个终端 `cd web && pnpm dev`(vite 独跑,
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
- // 无登录门禁:页面打开即用。数据访问鉴权由后端用 DATACLAW_API_KEY(建页人提供的 apikey)注入,
22
- // 见 dataclaw.client —— key 绝不下发浏览器。
18
+ // 页面无登录门禁,apikey 仅由后端使用。
23
19
 
24
- // 静态资源两条路(覆盖 FC 部署与本地 monorepo 启动两种场景):
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
- // Dev 模式 .env loader:`nest start --watch` 不会经过 `node --env-file=.env`,
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
- // 只调 app 自己的 BFF(/api/*),绝不直连 dataclaw —— apikey 在 server 侧注入,不进浏览器。
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,4 +1,3 @@
1
- // 纯 SVG 横向柱状图,不引第三方依赖。props { rows, labelField, valueField }。
2
1
  export default function BarChart({ rows, labelField, valueField }) {
3
2
  if (!rows || rows.length === 0) return null;
4
3
 
@@ -1,4 +1,3 @@
1
- // 结果卡片。props { rows, titleField, valueField }。数字按 zh-CN 千分位展示。
2
1
  export default function ResultCards({ rows, titleField, valueField }) {
3
2
  if (!rows || rows.length === 0) return <div className="empty">无数据</div>;
4
3
  return (
@@ -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
- // 内部字段:dataclaw 返回里不展示给用户的列(编号 / ID / 溯源元数据等)。
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', // dataclaw 每条返回都带的溯源元数据(source/record_type/fetched_at/trace_id),不展示
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
- # 发布/清理动手前把阿里云凭证准备好(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
- # ============================================================
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
- # 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)照样走这条。
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
- # 阿里云 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。
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
- # 调 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
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
- # 把 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 重装一次。
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
- # 别只说「检查凭证」——凭证不对只是可能之一,region 缺失 / CLI 装坏 / 网络不通报的是别的话。
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
- # 用 node 不用 python3:目标机器保证有 node(clawd 本身就是 node 跑的),不保证有 python
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
- # s 认的三个变量名就是它 access.yaml 里的键名(KEY_PAIR_IMPORTANT),大小写照它的
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
- # 老版本脚本(2026-09-19 之前)把凭证写在这里;本机残留的顺手删掉,别让一份过期 AK 躺在 persona 目录里
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
- # aliyun CLI 官方 release 兜底版本(仅在无 brew、走二进制下载时用;brew 路径不读它)。
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
- # 直接执行(bash ensure-toolchain.sh)= 自检;被 source 时不自动跑,留给调用方显式 ensure_toolchain。
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