@clawos-dev/clawd 0.2.143-beta.301.814d293 → 0.2.143-beta.303.ab28945

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 (31) hide show
  1. package/dist/cli.cjs +1364 -617
  2. package/dist/dispatch/mcp-server.cjs +39756 -0
  3. package/dist/persona-defaults/persona-app-builder/CLAUDE.md +3 -0
  4. package/dist/persona-defaults/persona-bug-fixer/CLAUDE.md +2 -0
  5. package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +29 -9
  6. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/README.md +10 -18
  7. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/config.env +2 -5
  8. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/contract/s.yaml.tmpl +1 -6
  9. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/ext.conf +3 -2
  10. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/.env.example +5 -14
  11. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/package.json +1 -3
  12. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +2 -2
  13. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.client.ts +18 -8
  14. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.config.ts +9 -12
  15. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.controller.ts +12 -8
  16. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/main.ts +3 -16
  17. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +1 -4
  18. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/api.js +4 -2
  19. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultTable.jsx +4 -2
  20. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/enums.js +3 -18
  21. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/pages/PerformanceQueryPage.jsx +35 -57
  22. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/hooks/pre-deploy.sh +16 -20
  23. package/dist/persona-defaults/persona-developer/CLAUDE.md +2 -0
  24. package/package.json +5 -2
  25. package/dist/persona-defaults/persona-dataclaw-builder/.secrets/dataclaw.env.example +0 -3
  26. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/auth/gate.ts +0 -70
  27. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/auth/session.ts +0 -52
  28. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/auth/ttc-user-info.ts +0 -23
  29. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/auth.js +0 -7
  30. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/FilterRow.jsx +0 -79
  31. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/SchemaFilters.jsx +0 -93
@@ -1,82 +1,51 @@
1
1
  import { useState } from 'react';
2
- import { runQuery } from '../api';
3
- import SchemaFilters from '../components/SchemaFilters';
2
+ import { queryPerformance } from '../api';
4
3
  import ResultTable from '../components/ResultTable';
5
- import BarChart from '../components/BarChart';
6
4
 
7
- // 数据集:起手页用 performance。agent 按用户需求改成
8
- // performance / consultant_progress / metadata_user 之一即可。
9
- // 数据范围(scope)不在前端选 —— 后端按建页人身份隐式裁剪。
10
- const DATASET = 'performance';
5
+ // 业绩查询起手页:对接 dataclaw 公网 /api/performance(必填 startDate/endDate)。
6
+ // agent 按建页人需求可改成 queryPpl / searchConsultants,或加收窄条件、换展示形态。
7
+ // 数据为只读全量(公网 apikey 即全量),无身份裁剪。
8
+ const DATE_FIELD_LABELS = { billing: 'Billing 业绩', invoice: '开票业绩', payment: '回款业绩' };
11
9
 
12
- // 聚合字段用真名。注意:`业绩所属二级团队` 是真实字段名,
13
- // 「顾问所属N级团队」只是它的 alias,查询 groupBy / aggregations 必须用真名。
14
- const GROUP_FIELD = '业绩所属二级团队';
15
- const METRIC = '业绩';
16
-
17
- // 预置一个 Billing 日期区间示例。
18
- // 提示:PPL 大范围查询很慢,务必带状态/时间区间(如 Billing 日期 between)收敛扫描范围。
19
- const INITIAL_FILTERS = [
20
- { field: 'Billing日期', op: 'between', value: ['2026-01-01', '2026-06-30'] },
21
- ];
22
-
23
- // 结果列名由后端决定(聚合后可能不叫原名),这里挑出展示用的标签列 / 数值列,兜底防错配。
24
- function pickFields(rows, preferredLabel, preferredValue) {
25
- const cols = Object.keys(rows[0]);
26
- const labelField = cols.includes(preferredLabel)
27
- ? preferredLabel
28
- : cols.find((c) => typeof rows[0][c] !== 'number') || cols[0];
29
- const valueField = cols.includes(preferredValue)
30
- ? preferredValue
31
- : cols.find((c) => typeof rows[0][c] === 'number' && c !== labelField) || cols[cols.length - 1];
32
- return { labelField, valueField };
33
- }
10
+ const fmt = (n) => (Number(n) || 0).toLocaleString('zh-CN');
34
11
 
35
12
  export default function PerformanceQueryPage() {
36
- const [filters, setFilters] = useState(INITIAL_FILTERS);
37
- const [groupBy, setGroupBy] = useState(GROUP_FIELD);
38
- const [rows, setRows] = useState([]);
39
- const [total, setTotal] = useState(0);
13
+ const [startDate, setStartDate] = useState('2026-05-01');
14
+ const [endDate, setEndDate] = useState('2026-05-31');
15
+ const [result, setResult] = useState(null);
40
16
  const [loading, setLoading] = useState(false);
41
17
  const [error, setError] = useState('');
42
- const [ran, setRan] = useState(false);
43
18
 
44
19
  const onQuery = async () => {
20
+ if (!startDate || !endDate) { setError('请选择起止日期'); return; }
45
21
  setLoading(true);
46
22
  setError('');
47
23
  try {
48
- const json = await runQuery({
49
- dataset: DATASET,
50
- filters,
51
- groupBy,
52
- aggregations: [{ field: METRIC, op: 'sum' }],
53
- orderBy: { field: METRIC, direction: 'desc' },
54
- limit: 50,
55
- });
56
- setRows(json.data?.rows || []);
57
- setTotal(json.data?.total || 0);
58
- setRan(true);
24
+ setResult(await queryPerformance({ startDate, endDate }));
59
25
  } catch (e) {
60
26
  setError(e.message || '查询失败');
61
- setRows([]);
27
+ setResult(null);
62
28
  } finally {
63
29
  setLoading(false);
64
30
  }
65
31
  };
66
32
 
67
- const picked = rows.length ? pickFields(rows, groupBy, METRIC) : null;
33
+ const byDateField = result?.byDateField || null;
68
34
 
69
35
  return (
70
36
  <div className="page">
71
37
  <h1>业绩查询(示例)</h1>
72
- <p className="hint">数据集:{DATASET}。数据范围由后端按你的身份自动裁剪。</p>
38
+ <p className="hint">数据来自 dataclaw 业绩库,按日期区间查询(只读)。</p>
73
39
 
74
40
  <section className="panel">
75
41
  <label className="group-by">
76
- 分组字段
77
- <input value={groupBy} onChange={(e) => setGroupBy(e.target.value)} />
42
+ 起始日期
43
+ <input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
44
+ </label>
45
+ <label className="group-by">
46
+ 结束日期
47
+ <input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
78
48
  </label>
79
- <SchemaFilters dataset={DATASET} filters={filters} onChange={setFilters} />
80
49
  <button type="button" className="btn-primary" onClick={onQuery} disabled={loading}>
81
50
  {loading ? '查询中…' : '查询'}
82
51
  </button>
@@ -84,17 +53,26 @@ export default function PerformanceQueryPage() {
84
53
 
85
54
  {error && <div className="error">{error}</div>}
86
55
 
87
- {ran && !error && (
56
+ {result && !error && (
88
57
  <section className="results">
89
58
  <div className="results-head">
90
- {total.toLocaleString('zh-CN')} 条,按 {METRIC} 汇总(Top {rows.length}
59
+ {result.queryRange || `${startDate} ~ ${endDate}`} 共 {fmt(result.total)}
91
60
  </div>
92
- {picked && rows.length > 0 && (
93
- <div className="chart-wrap">
94
- <BarChart rows={rows} labelField={picked.labelField} valueField={picked.valueField} />
61
+
62
+ {byDateField && (
63
+ <div className="cards">
64
+ {Object.entries(byDateField).map(([k, v]) => (
65
+ <div className="card" key={k}>
66
+ <div className="card-title">{DATE_FIELD_LABELS[k] || k}({fmt(v?.total)} 条)</div>
67
+ <div className="card-value">{fmt(v?.amount)}</div>
68
+ </div>
69
+ ))}
95
70
  </div>
96
71
  )}
97
- <ResultTable rows={rows} />
72
+
73
+ <div className="chart-wrap" style={{ overflowX: 'auto' }}>
74
+ <ResultTable rows={result.items || []} />
75
+ </div>
98
76
  </section>
99
77
  )}
100
78
  </div>
@@ -1,25 +1,21 @@
1
1
  #!/usr/bin/env bash
2
- # dataclaw 发布前置:校验 persona 密钥、生成/推导运行时变量并 export,供通用占位符替换取用。
3
- # 由 deploy-kit/publish.sh aliyun 就绪后、渲染 s.yaml 前 source(共享 EXT_DIR/APP_NAME/ACC/REGION/PERSONA_KIT 作用域)。
2
+ # dataclaw 发布前置:从生成 app server/.env 读出建页人填的 apikey,export 供 s.yaml 占位符替换。
3
+ # 由 deploy-kit/publish.sh 在渲染 s.yaml 前 source(共享 EXT_DIR/APP_NAME/PERSONA_KIT 等作用域)。
4
4
 
5
- # dataclaw 服务间密钥(现读,不进 git);缺失则拒发布(app 没它查不到数)。
6
- # 密钥在 persona-root .secrets/(example/gitignore 白名单都在这一层),不是 extension-kit/.secrets/。
7
- [ -f "$PERSONA_ROOT/.secrets/dataclaw.env" ] || {
8
- echo "❌ $PERSONA_ROOT/.secrets/dataclaw.env(含 DATACLAW_INTERNAL_SECRET)。参考 $PERSONA_ROOT/.secrets/dataclaw.env.example 填写。" >&2
5
+ # 建页人提供的 dataclaw apikey 写在生成 app 的 server/.env(DATACLAW_API_KEY)。
6
+ # .env server/.fcignore 排除、不进部署包,所以这里读出来、经 s.yaml 注入 FC 环境变量。
7
+ ENV_FILE="$EXT_DIR/server/.env"
8
+ if [ -f "$ENV_FILE" ]; then
9
+ # 只取 DATACLAW_API_KEY 一行、剥掉可能的引号;不 source 整个 .env(避免污染其它变量)。
10
+ # || true:publish.sh 跑在 set -euo pipefail 下,grep 无命中(有 .env 但缺 key 行)会让管道返回非 0
11
+ # → 不加会静默死在这行、走不到下面的 fail-loud 友好报错。统一收敛到下方空值检查。
12
+ DATACLAW_API_KEY="$(grep -E '^DATACLAW_API_KEY=' "$ENV_FILE" | head -1 | cut -d= -f2- | sed -e 's/^["'\'']//' -e 's/["'\'']$//' || true)"
13
+ fi
14
+ if [ -z "${DATACLAW_API_KEY:-}" ]; then
15
+ echo "❌ 缺 DATACLAW_API_KEY:请在 $ENV_FILE 填入建页人的 dataclaw apikey(只填冒号后的 secret 段)。" >&2
9
16
  exit 1
10
- }
11
- set -a; . "$PERSONA_ROOT/.secrets/dataclaw.env"; set +a
12
-
13
- # SESSION_SECRET:每个 app 一份、跨重部署稳定(否则重发后所有人 session 失效)。
14
- # 首次发布生成并持久化到 ext.conf;之后 source ext.conf 已带上。
15
- if [ -z "${SESSION_SECRET:-}" ]; then
16
- SESSION_SECRET="$(openssl rand -hex 32)"
17
- echo "SESSION_SECRET=$SESSION_SECRET" >> "$EXT_DIR/ext.conf"
18
17
  fi
19
- export SESSION_SECRET
20
-
21
- # APP_BASE_URL:飞书登录回调用。fc3 自定义域名由 APP_NAME+ACC+REGION 推导,用 https(cookie secure 要求)。
22
- export APP_BASE_URL="https://${APP_NAME}.fcv3.${ACC}.${REGION}.fc.devsapp.net"
18
+ export DATACLAW_API_KEY
23
19
 
24
- # 可选占位符显式兜底(未填时通用扫描需「已定义」才放行)
25
- export DATACLAW_AS_UNION_ID="${DATACLAW_AS_UNION_ID:-}"
20
+ # service url:config.env / ext.conf 已 source 进来;兜底公网地址。
21
+ export DATACLAW_SERVICE_URL="${DATACLAW_SERVICE_URL:-https://120.48.21.36}"
@@ -115,3 +115,5 @@ done
115
115
  ⚠️ **`.mcp.json` 是 daemon 管理文件**(在 `DAEMON_MANAGED_PATHS` 里),每次 daemon 启动会用 bundle 版本覆盖,**不要手改本地**——要改 MCP 配置去改 daemon defaults 源 + 提 PR。
116
116
 
117
117
  **要加别的 MCP server**(项目专属 / 个人用):在**项目目录**(不是 persona 目录)下加 `.mcp.json`,跟 persona 这份不冲突。或者扩 daemon defaults 给所有用户。
118
+
119
+ > dispatch 教学(`@persona/<id>` 委派 / `personaDispatchComplete` 回报)由 daemon 通过 `--append-system-prompt` 给所有 cc spawn 统一注入,不在这份 CLAUDE.md 重复。源见 `daemon/src/dispatch/system-prompt.ts`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.143-beta.301.814d293",
3
+ "version": "0.2.143-beta.303.ab28945",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@clawos/clawd-protocol": "workspace:*",
30
+ "@clawos/log-client": "workspace:*",
30
31
  "@types/diff": "^7.0.2",
31
32
  "@types/node": "^22.0.0",
32
33
  "@types/uuid": "^10.0.0",
@@ -48,8 +49,10 @@
48
49
  },
49
50
  "dependencies": {
50
51
  "@lydell/node-pty": "1.2.0-beta.12",
52
+ "@modelcontextprotocol/sdk": "^1.29.0",
51
53
  "@xterm/addon-serialize": "^0.14.0",
52
54
  "@xterm/headless": "^6.0.0",
53
- "jszip": "^3.10.1"
55
+ "jszip": "^3.10.1",
56
+ "undici": "^6"
54
57
  }
55
58
  }
@@ -1,3 +0,0 @@
1
- # dataclaw 服务间共享密钥(= dataclaw-service 的 SEND_AS_BOT_SHARED_SECRET)。
2
- # 复制为同目录 dataclaw.env 并填实际值;dataclaw.env 不进 git(见 .gitignore)。
3
- DATACLAW_INTERNAL_SECRET=replace-me
@@ -1,70 +0,0 @@
1
- // TTC 飞书登录门禁(无密钥经纪):Express 中间件 + /auth/login、/auth/callback 处理器。
2
- // 必须挂在 vite middleware(dev) / express.static(prod) 之前——前端 HTML 不走 Nest 路由,
3
- // Nest guard 拦不住,所以门禁只能是 Express 层中间件(main.ts 接线在 vite/static 之前)。
4
- // 门禁只决定"能不能打开页";数据范围仍由 dataclaw client 固定用建页人 union_id(PR-4 已做),
5
- // 不用登录者身份查数。
6
- import { fetchTtcUser } from './ttc-user-info'
7
- import { makeSession, readSession, makeState, verifyState } from './session'
8
-
9
- // req/res/next 用 any:express 不是本 server 包的直接依赖(pnpm 下从 server 根 require.resolve
10
- // 'express' 失败,main.ts:67 同样用 any),从 'express' import 类型会编译失败。
11
- type Req = any
12
- type Res = any
13
- type Next = (err?: unknown) => void
14
-
15
- const SESSION_COOKIE = 'dc_session'
16
- const STATE_COOKIE = 'dc_oauth_state'
17
-
18
- // 子路径部署(clawd dev preview/<port>/,见 main.ts subPath 逻辑)下 redirect 要带同样前缀;
19
- // prod base=/ 时无前缀,返回 '/auth/login'。
20
- function loginPath(req: Req): string {
21
- const url = String(req.originalUrl || req.url || '')
22
- const m = url.match(/^\/(preview\/\d+)\//)
23
- return m ? `/${m[1]}/auth/login` : '/auth/login'
24
- }
25
-
26
- // 门禁中间件:放行 /auth/*;DATACLAW_AUTH_DISABLED=1 全放行;否则无有效 session → 302 /auth/login。
27
- export function authGate(req: Req, res: Res, next: Next) {
28
- if (process.env.DATACLAW_AUTH_DISABLED === '1') return next()
29
- const p = String(req.path || req.url || '')
30
- if (p.includes('/auth/login') || p.includes('/auth/callback')) return next()
31
- const sess = readSession(req.cookies?.[SESSION_COOKIE])
32
- if (sess) return next()
33
- return res.redirect(302, loginPath(req))
34
- }
35
-
36
- export function loginHandler(req: Req, res: Res) {
37
- const authBase = process.env.TTC_AUTH_BASE || 'https://app.ttcadvisory.com'
38
- const appBase = (process.env.APP_BASE_URL || '').replace(/\/+$/, '')
39
- const state = makeState()
40
- res.cookie(STATE_COOKIE, state, { httpOnly: true, sameSite: 'lax', secure: true, maxAge: 5 * 60 * 1000 })
41
- const url = new URL('/auth/authorize', authBase)
42
- url.searchParams.set('callback_url', `${appBase}/auth/callback`)
43
- url.searchParams.set('state', state)
44
- url.searchParams.set('auto', '1')
45
- res.redirect(302, url.toString())
46
- }
47
-
48
- export async function callbackHandler(req: Req, res: Res) {
49
- const token = String(req.query?.token || '')
50
- const state = String(req.query?.state || '')
51
- const cookieState = req.cookies?.[STATE_COOKIE]
52
- // CSRF:回调 state 必须等于 cookie 里的、且签名有效未过期
53
- if (!token || !state || state !== cookieState || !verifyState(state)) {
54
- return res.status(401).send('登录校验失败,请重试')
55
- }
56
- try {
57
- const apiBase = process.env.TTC_API_BASE || 'https://api.ttcadvisory.com'
58
- const user = await fetchTtcUser(apiBase, token) // 只验身份;TTC token 验完即丢,不存不用于查数
59
- res.clearCookie(STATE_COOKIE)
60
- res.cookie(SESSION_COOKIE, makeSession(user.uniqueId, user.name), {
61
- httpOnly: true,
62
- sameSite: 'lax',
63
- secure: true,
64
- maxAge: 12 * 3600 * 1000,
65
- })
66
- return res.redirect(302, '/')
67
- } catch {
68
- return res.status(401).send('飞书身份校验失败')
69
- }
70
- }
@@ -1,52 +0,0 @@
1
- // HMAC 签名 session + oauth state cookie,用 SESSION_SECRET 签发/校验。
2
- // FC 多实例无共享内存,所以 state 也走签名 cookie,不能用进程内 Map。
3
- import { createHmac, timingSafeEqual, randomBytes } from 'node:crypto'
4
-
5
- const SECRET = () => process.env.SESSION_SECRET || ''
6
-
7
- function sign(payload: string): string {
8
- const mac = createHmac('sha256', SECRET()).update(payload).digest('base64url')
9
- return `${Buffer.from(payload).toString('base64url')}.${mac}`
10
- }
11
-
12
- function verify(token: string): string | null {
13
- const [b64, mac] = (token || '').split('.')
14
- if (!b64 || !mac) return null
15
- const payload = Buffer.from(b64, 'base64url').toString()
16
- const expect = createHmac('sha256', SECRET()).update(payload).digest('base64url')
17
- const a = Buffer.from(mac)
18
- const b = Buffer.from(expect)
19
- if (a.length !== b.length || !timingSafeEqual(a, b)) return null
20
- return payload
21
- }
22
-
23
- // session:存 {uid,name,exp}(仅展示/审计,不用于查数——数据范围恒用建页人 union_id)。
24
- export function makeSession(uid: string, name: string, ttlMs = 12 * 3600 * 1000): string {
25
- return sign(JSON.stringify({ uid, name, exp: Date.now() + ttlMs }))
26
- }
27
-
28
- export function readSession(token?: string): { uid: string; name: string } | null {
29
- const p = token && verify(token)
30
- if (!p) return null
31
- try {
32
- const o = JSON.parse(p)
33
- if (typeof o.exp === 'number' && o.exp > Date.now()) return { uid: o.uid, name: o.name }
34
- } catch {}
35
- return null
36
- }
37
-
38
- export function makeState(): string {
39
- const nonce = randomBytes(16).toString('base64url')
40
- return sign(JSON.stringify({ nonce, exp: Date.now() + 5 * 60 * 1000 }))
41
- }
42
-
43
- export function verifyState(token?: string): boolean {
44
- const p = token && verify(token)
45
- if (!p) return false
46
- try {
47
- const o = JSON.parse(p)
48
- return typeof o.exp === 'number' && o.exp > Date.now()
49
- } catch {
50
- return false
51
- }
52
- }
@@ -1,23 +0,0 @@
1
- // 用 TTC JWT 调 user_info 验身份(无密钥经纪:app 不持飞书/TTC 密钥,只用回调 token 验身份)。
2
- // 精简自 daemon feishu-auth/ttc-client.ts,独立实现、不 import daemon 代码。
3
- // 接口响应结构:GET /api/user_service/v1/login/user → { code:0, data:{ unique_id, name, union_id, avatar_url } }
4
- export interface TtcUser {
5
- uniqueId: string
6
- name: string
7
- unionId?: string
8
- avatarUrl?: string
9
- }
10
-
11
- export async function fetchTtcUser(apiBase: string, token: string, fetchImpl: typeof fetch = fetch): Promise<TtcUser> {
12
- const res = await fetchImpl(`${apiBase}/api/user_service/v1/login/user`, { headers: { Authorization: `Bearer ${token}` } })
13
- if (!res.ok) throw new Error(`TTC user_info HTTP ${res.status}`)
14
- const body = (await res.json()) as { code?: number; data?: { unique_id?: string; name?: string; union_id?: string; avatar_url?: string } }
15
- if (body.code !== 0 || !body.data?.unique_id || !body.data?.name) throw new Error('TTC user_info invalid')
16
- const d = body.data
17
- return {
18
- uniqueId: d.unique_id,
19
- name: d.name,
20
- ...(d.union_id ? { unionId: d.union_id } : {}),
21
- ...(d.avatar_url ? { avatarUrl: d.avatar_url } : {}),
22
- }
23
- }
@@ -1,7 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- // 门禁 hook 占位:真实飞书登录态检测在 PR-6。返回 { ready, authed }。
3
- export function useAuthGate() {
4
- const [state, setState] = useState({ ready: false, authed: false });
5
- useEffect(() => { setState({ ready: true, authed: true }); }, []); // 占位:默认已登录
6
- return state;
7
- }
@@ -1,79 +0,0 @@
1
- import { ENUM_HINTS } from '../enums';
2
-
3
- // 操作符中文标签(仅展示用,没命中就回退显示原始 code)。
4
- const OP_LABELS = {
5
- eq: '等于', ne: '不等于', contains: '包含',
6
- gt: '大于', gte: '大于等于', lt: '小于', lte: '小于等于',
7
- between: '区间', in: '属于', notin: '不属于',
8
- };
9
-
10
- // 单条筛选。受控:props { field(来自 /api/schema 的字段元数据), value:{field,op,value}, onChange, onRemove }。
11
- // 控件选择规则:
12
- // - op === 'between' → 两个输入框
13
- // - op === 'in' / 'notin' → 单输入框,逗号分隔(值为数组)
14
- // - ENUM_HINTS 命中该字段 → 下拉,选项来自 ENUM_HINTS[sourceField]
15
- // - 否则按 field.type → string=text / number=number / date=date
16
- // 操作符下拉选项直接来自 field.operators。
17
- export default function FilterRow({ field, value, onChange, onRemove }) {
18
- const type = field.type || 'string';
19
- const enumValues = ENUM_HINTS[field.sourceField];
20
- const ops = field.operators && field.operators.length ? field.operators : [value.op].filter(Boolean);
21
- const inputType = type === 'number' ? 'number' : type === 'date' ? 'date' : 'text';
22
- const label = (field.aliases && field.aliases[0]) || field.sourceField;
23
-
24
- const coerce = (raw) => (type === 'number' && raw !== '' ? Number(raw) : raw);
25
- const set = (patch) => onChange({ ...value, ...patch });
26
-
27
- // 切操作符时 value 形态可能要换(标量 / 二元区间 / 数组),统一重置避免形态错配。
28
- const onOpChange = (e) => {
29
- const op = e.target.value;
30
- const next = op === 'between' ? ['', ''] : op === 'in' || op === 'notin' ? [] : '';
31
- set({ op, value: next });
32
- };
33
-
34
- const renderValueControl = () => {
35
- if (value.op === 'between') {
36
- const v = Array.isArray(value.value) ? value.value : ['', ''];
37
- return (
38
- <span className="filter-between">
39
- <input type={inputType} value={v[0] ?? ''} onChange={(e) => set({ value: [coerce(e.target.value), v[1] ?? ''] })} />
40
- <span className="sep">~</span>
41
- <input type={inputType} value={v[1] ?? ''} onChange={(e) => set({ value: [v[0] ?? '', coerce(e.target.value)] })} />
42
- </span>
43
- );
44
- }
45
- if (value.op === 'in' || value.op === 'notin') {
46
- const text = Array.isArray(value.value) ? value.value.join(',') : value.value ?? '';
47
- return (
48
- <input
49
- className="filter-value"
50
- placeholder="多个值用逗号分隔"
51
- value={text}
52
- onChange={(e) => set({ value: e.target.value.split(',').map((s) => coerce(s.trim())).filter((s) => s !== '') })}
53
- />
54
- );
55
- }
56
- if (enumValues) {
57
- return (
58
- <select className="filter-value" value={value.value ?? ''} onChange={(e) => set({ value: e.target.value })}>
59
- <option value="">请选择</option>
60
- {enumValues.map((opt) => <option key={opt} value={opt}>{opt}</option>)}
61
- </select>
62
- );
63
- }
64
- return (
65
- <input className="filter-value" type={inputType} value={value.value ?? ''} onChange={(e) => set({ value: coerce(e.target.value) })} />
66
- );
67
- };
68
-
69
- return (
70
- <div className="filter-row">
71
- <span className="filter-label">{label}</span>
72
- <select className="filter-op" value={value.op} onChange={onOpChange}>
73
- {ops.map((op) => <option key={op} value={op}>{OP_LABELS[op] || op}</option>)}
74
- </select>
75
- {renderValueControl()}
76
- <button type="button" className="filter-remove" onClick={onRemove}>删除</button>
77
- </div>
78
- );
79
- }
@@ -1,93 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import { getSchema } from '../api';
3
- import { INTERNAL_FIELDS, ENUM_HINTS, TYPE_DEFAULT_OP } from '../enums';
4
- import FilterRow from './FilterRow';
5
-
6
- // 给 schema 没覆盖到的字段(如预置的 Billing日期)按字段名兜底猜类型。
7
- function guessType(sourceField) {
8
- if (/日期|时间|date|time/i.test(sourceField)) return 'date';
9
- return 'string';
10
- }
11
-
12
- const FALLBACK_OPS = {
13
- string: ['contains', 'eq', 'ne', 'in', 'notin'],
14
- number: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notin'],
15
- date: ['eq', 'gt', 'gte', 'lt', 'lte', 'between'],
16
- };
17
-
18
- function initValue(op) {
19
- if (op === 'between') return ['', ''];
20
- if (op === 'in' || op === 'notin') return [];
21
- return '';
22
- }
23
-
24
- // 受控筛选区。拉 /api/schema 拿字段元数据,过滤掉 INTERNAL_FIELDS,
25
- // 渲染已选 filters(FilterRow)+ 「+ 添加筛选字段」下拉。
26
- // props { dataset, filters, onChange }。
27
- export default function SchemaFilters({ dataset, filters, onChange }) {
28
- const [fields, setFields] = useState([]);
29
- const [loading, setLoading] = useState(true);
30
- const [error, setError] = useState('');
31
-
32
- useEffect(() => {
33
- let alive = true;
34
- setLoading(true);
35
- setError('');
36
- getSchema(dataset)
37
- .then((json) => {
38
- if (!alive) return;
39
- const data = json.data;
40
- // 传了 dataset 时 data 可能是单对象;否则是数组。
41
- const entry = Array.isArray(data) ? data.find((d) => d.dataset === dataset) : data;
42
- const fs = (entry?.fields || []).filter((f) => !INTERNAL_FIELDS.has(f.sourceField));
43
- setFields(fs);
44
- })
45
- .catch((e) => { if (alive) setError(e.message || '加载 schema 失败'); })
46
- .finally(() => { if (alive) setLoading(false); });
47
- return () => { alive = false; };
48
- }, [dataset]);
49
-
50
- // 取字段元数据:schema 里找得到用真的,找不到(预置 filter / 未加载)给兜底。
51
- const metaFor = (sourceField) => {
52
- const found = fields.find((f) => f.sourceField === sourceField);
53
- if (found) return found;
54
- const type = ENUM_HINTS[sourceField] ? 'string' : guessType(sourceField);
55
- return { sourceField, type, operators: FALLBACK_OPS[type], aliases: [] };
56
- };
57
-
58
- const update = (i, next) => onChange(filters.map((f, idx) => (idx === i ? next : f)));
59
- const remove = (i) => onChange(filters.filter((_, idx) => idx !== i));
60
- const add = (e) => {
61
- const sourceField = e.target.value;
62
- e.target.value = '';
63
- if (!sourceField) return;
64
- const meta = metaFor(sourceField);
65
- const wanted = TYPE_DEFAULT_OP[meta.type];
66
- const op = wanted && meta.operators?.includes(wanted) ? wanted : meta.operators?.[0] || 'eq';
67
- onChange([...filters, { field: sourceField, op, value: initValue(op) }]);
68
- };
69
-
70
- if (error) return <div className="error">筛选项加载失败:{error}</div>;
71
-
72
- return (
73
- <div className="schema-filters">
74
- {filters.map((f, i) => (
75
- <FilterRow
76
- key={i}
77
- field={metaFor(f.field)}
78
- value={f}
79
- onChange={(next) => update(i, next)}
80
- onRemove={() => remove(i)}
81
- />
82
- ))}
83
- <select className="filter-add" value="" onChange={add} disabled={loading}>
84
- <option value="">{loading ? '加载字段中…' : '+ 添加筛选字段'}</option>
85
- {fields.map((f) => (
86
- <option key={f.sourceField} value={f.sourceField}>
87
- {(f.aliases && f.aliases[0]) || f.sourceField}
88
- </option>
89
- ))}
90
- </select>
91
- </div>
92
- );
93
- }