@clawos-dev/clawd 0.2.142 → 0.2.143-beta.301.814d293
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/cli.cjs +920 -1562
- package/dist/deploy-kit/.secrets/aliyun.env.example +4 -0
- package/dist/deploy-kit/scripts/new-extension.sh +22 -0
- package/dist/deploy-kit/scripts/publish.sh +100 -0
- package/dist/deploy-kit/scripts/publish.spec.ts +69 -0
- package/dist/{persona-defaults/persona-app-builder/extension-kit → deploy-kit}/scripts/remove-extension.sh +15 -26
- package/dist/persona-defaults/persona-app-builder/CLAUDE.md +13 -16
- package/dist/persona-defaults/persona-app-builder/extension-kit/README.md +24 -18
- package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/ext.conf +5 -0
- package/dist/persona-defaults/persona-bug-fixer/CLAUDE.md +0 -2
- package/dist/persona-defaults/persona-dataclaw-builder/.mcp.json +4 -0
- package/dist/persona-defaults/persona-dataclaw-builder/.secrets/dataclaw.env.example +3 -0
- package/dist/persona-defaults/persona-dataclaw-builder/CLAUDE.md +66 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/README.md +99 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/config.env +20 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/contract/s.yaml.tmpl +59 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/ext.conf +5 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/.env.example +15 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/.fcignore +7 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/nest-cli.json +8 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/package.json +29 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +13 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/auth/gate.ts +70 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/auth/session.ts +52 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/auth/ttc-user-info.ts +23 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.client.ts +83 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.config.ts +18 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.controller.ts +30 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.module.ts +9 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/main.ts +116 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +6 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/tsconfig.json +14 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/index.html +12 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/package.json +18 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +9 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/api.js +19 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/auth.js +7 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/BarChart.jsx +38 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/FilterRow.jsx +79 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultCards.jsx +17 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultTable.jsx +21 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/SchemaFilters.jsx +93 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/enums.js +22 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/main.jsx +6 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/pages/PerformanceQueryPage.jsx +102 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/styles.css +83 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/vite.config.js +30 -0
- package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/hooks/pre-deploy.sh +25 -0
- package/dist/persona-defaults/persona-developer/CLAUDE.md +0 -2
- package/package.json +2 -5
- package/dist/dispatch/mcp-server.cjs +0 -39756
- package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/new-extension.sh +0 -66
- package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/publish.sh +0 -110
- /package/dist/{persona-defaults/persona-app-builder → deploy-kit}/.secrets/aliyun.env +0 -0
- /package/dist/{persona-defaults/persona-app-builder/extension-kit → deploy-kit}/contract/bootstrap +0 -0
- /package/dist/{persona-defaults/persona-app-builder/extension-kit → deploy-kit}/scripts/ensure-toolchain.sh +0 -0
- /package/dist/{persona-defaults/persona-app-builder/extension-kit → deploy-kit}/scripts/verify.sh +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 只调 app 自己的 BFF(/api/*),绝不直连 dataclaw —— 密钥/身份都在 server 侧注入,不进浏览器。
|
|
2
|
+
//
|
|
3
|
+
// dev 隧道模式下页面挂在子路径 /preview/<port>/ 下(见 vite.config.js base),
|
|
4
|
+
// 所以 API_BASE 要拼上 import.meta.env.BASE_URL,否则 fetch 会打到错误的绝对路径。
|
|
5
|
+
const API_BASE = (import.meta.env.BASE_URL || '/').replace(/\/+$/, '') + '/api';
|
|
6
|
+
|
|
7
|
+
async function req(path, opts = {}) {
|
|
8
|
+
const res = await fetch(API_BASE + path, {
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
...opts,
|
|
11
|
+
});
|
|
12
|
+
const json = await res.json().catch(() => ({}));
|
|
13
|
+
if (!res.ok || json.ok === false) throw new Error(json.error || `请求失败 ${res.status}`);
|
|
14
|
+
return json;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getSchema = (dataset) => req(`/schema?dataset=${encodeURIComponent(dataset || '')}`);
|
|
18
|
+
export const runQuery = (body) => req('/query', { method: 'POST', body: JSON.stringify(body) });
|
|
19
|
+
export const searchConsultants = (kw) => req(`/consultants?keyword=${encodeURIComponent(kw)}`);
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 纯 SVG 横向柱状图,不引第三方依赖。props { rows, labelField, valueField }。
|
|
2
|
+
export default function BarChart({ rows, labelField, valueField }) {
|
|
3
|
+
if (!rows || rows.length === 0) return null;
|
|
4
|
+
|
|
5
|
+
const data = rows.map((r) => ({
|
|
6
|
+
label: String(r[labelField] ?? ''),
|
|
7
|
+
value: Number(r[valueField]) || 0,
|
|
8
|
+
}));
|
|
9
|
+
const max = Math.max(...data.map((d) => d.value), 0) || 1;
|
|
10
|
+
|
|
11
|
+
const rowH = 24;
|
|
12
|
+
const gap = 8;
|
|
13
|
+
const labelW = 150;
|
|
14
|
+
const barAreaW = 360;
|
|
15
|
+
const valueW = 120;
|
|
16
|
+
const width = labelW + barAreaW + valueW;
|
|
17
|
+
const height = data.length * (rowH + gap) + gap;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<svg width={width} height={height} role="img" aria-label="柱状图" style={{ maxWidth: '100%' }}>
|
|
21
|
+
{data.map((d, i) => {
|
|
22
|
+
const y = gap + i * (rowH + gap);
|
|
23
|
+
const w = Math.max(0, (d.value / max) * barAreaW);
|
|
24
|
+
return (
|
|
25
|
+
<g key={i}>
|
|
26
|
+
<text x={labelW - 8} y={y + rowH / 2} textAnchor="end" dominantBaseline="middle" fontSize="12" fill="#374151">
|
|
27
|
+
{d.label}
|
|
28
|
+
</text>
|
|
29
|
+
<rect x={labelW} y={y} width={w} height={rowH} rx="3" fill="#2563eb" />
|
|
30
|
+
<text x={labelW + w + 6} y={y + rowH / 2} dominantBaseline="middle" fontSize="12" fill="#1f2328">
|
|
31
|
+
{d.value.toLocaleString('zh-CN')}
|
|
32
|
+
</text>
|
|
33
|
+
</g>
|
|
34
|
+
);
|
|
35
|
+
})}
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// 结果卡片。props { rows, titleField, valueField }。数字按 zh-CN 千分位展示。
|
|
2
|
+
export default function ResultCards({ rows, titleField, valueField }) {
|
|
3
|
+
if (!rows || rows.length === 0) return <div className="empty">无数据</div>;
|
|
4
|
+
return (
|
|
5
|
+
<div className="cards">
|
|
6
|
+
{rows.map((r, i) => {
|
|
7
|
+
const v = r[valueField];
|
|
8
|
+
return (
|
|
9
|
+
<div className="card" key={i}>
|
|
10
|
+
<div className="card-title">{String(r[titleField] ?? '')}</div>
|
|
11
|
+
<div className="card-value">{typeof v === 'number' ? v.toLocaleString('zh-CN') : String(v ?? '')}</div>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
})}
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// 结果表格。列取 rows[0] 的 key,数字按 zh-CN 千分位展示,空数组显示「无数据」。
|
|
2
|
+
export default function ResultTable({ rows }) {
|
|
3
|
+
if (!rows || rows.length === 0) return <div className="empty">无数据</div>;
|
|
4
|
+
const cols = Object.keys(rows[0]);
|
|
5
|
+
return (
|
|
6
|
+
<table className="result-table">
|
|
7
|
+
<thead>
|
|
8
|
+
<tr>{cols.map((c) => <th key={c}>{c}</th>)}</tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody>
|
|
11
|
+
{rows.map((r, i) => (
|
|
12
|
+
<tr key={i}>
|
|
13
|
+
{cols.map((c) => (
|
|
14
|
+
<td key={c}>{typeof r[c] === 'number' ? r[c].toLocaleString('zh-CN') : String(r[c] ?? '')}</td>
|
|
15
|
+
))}
|
|
16
|
+
</tr>
|
|
17
|
+
))}
|
|
18
|
+
</tbody>
|
|
19
|
+
</table>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// 枚举可选值 / 内部字段 —— 逐字对齐后端 query-filters.js。
|
|
2
|
+
//
|
|
3
|
+
// 为什么前端要自带这份常量:
|
|
4
|
+
// - /api/schema 只给 field.type / field.operators / aliases,**不给枚举候选值**,
|
|
5
|
+
// 所以状态类字段的下拉选项只能在前端硬编码(ENUM_HINTS)。
|
|
6
|
+
// - describeDatasets 仍会返回 ID / 各级编号 等内部字段,前端要主动过滤掉不渲染
|
|
7
|
+
// (INTERNAL_FIELDS)。
|
|
8
|
+
// 后端 query-filters.js 改了这里也要同步改。
|
|
9
|
+
|
|
10
|
+
export const ENUM_HINTS = {
|
|
11
|
+
候选人状态: ['推荐', '1面', '2面', '3面', '4面', '终面', 'Offer', '已入职', '淘汰'],
|
|
12
|
+
顾问类型: ['正式', '实习', '外部合作顾问', '兼职', '外包', '虚拟账号'],
|
|
13
|
+
顾问在职状态: ['在职', '离职'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const INTERNAL_FIELDS = new Set([
|
|
17
|
+
'ID', 'unique_id', '链接', 'SourceID', '发票ID', '客户ID', '职位ID',
|
|
18
|
+
'业绩所属一级团队编号', '业绩所属二级团队编号', '业绩所属三级团队编号', '所属OP名称', '候选人ID',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// 每种字段类型添加筛选时的默认操作符。
|
|
22
|
+
export const TYPE_DEFAULT_OP = { string: 'contains', number: 'eq', date: 'eq' };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { runQuery } from '../api';
|
|
3
|
+
import SchemaFilters from '../components/SchemaFilters';
|
|
4
|
+
import ResultTable from '../components/ResultTable';
|
|
5
|
+
import BarChart from '../components/BarChart';
|
|
6
|
+
|
|
7
|
+
// 数据集:起手页用 performance。agent 按用户需求改成
|
|
8
|
+
// performance / consultant_progress / metadata_user 之一即可。
|
|
9
|
+
// 数据范围(scope)不在前端选 —— 后端按建页人身份隐式裁剪。
|
|
10
|
+
const DATASET = 'performance';
|
|
11
|
+
|
|
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
|
+
}
|
|
34
|
+
|
|
35
|
+
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);
|
|
40
|
+
const [loading, setLoading] = useState(false);
|
|
41
|
+
const [error, setError] = useState('');
|
|
42
|
+
const [ran, setRan] = useState(false);
|
|
43
|
+
|
|
44
|
+
const onQuery = async () => {
|
|
45
|
+
setLoading(true);
|
|
46
|
+
setError('');
|
|
47
|
+
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);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
setError(e.message || '查询失败');
|
|
61
|
+
setRows([]);
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const picked = rows.length ? pickFields(rows, groupBy, METRIC) : null;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="page">
|
|
71
|
+
<h1>业绩查询(示例)</h1>
|
|
72
|
+
<p className="hint">数据集:{DATASET}。数据范围由后端按你的身份自动裁剪。</p>
|
|
73
|
+
|
|
74
|
+
<section className="panel">
|
|
75
|
+
<label className="group-by">
|
|
76
|
+
分组字段
|
|
77
|
+
<input value={groupBy} onChange={(e) => setGroupBy(e.target.value)} />
|
|
78
|
+
</label>
|
|
79
|
+
<SchemaFilters dataset={DATASET} filters={filters} onChange={setFilters} />
|
|
80
|
+
<button type="button" className="btn-primary" onClick={onQuery} disabled={loading}>
|
|
81
|
+
{loading ? '查询中…' : '查询'}
|
|
82
|
+
</button>
|
|
83
|
+
</section>
|
|
84
|
+
|
|
85
|
+
{error && <div className="error">{error}</div>}
|
|
86
|
+
|
|
87
|
+
{ran && !error && (
|
|
88
|
+
<section className="results">
|
|
89
|
+
<div className="results-head">
|
|
90
|
+
共 {total.toLocaleString('zh-CN')} 条,按 {METRIC} 汇总(Top {rows.length})
|
|
91
|
+
</div>
|
|
92
|
+
{picked && rows.length > 0 && (
|
|
93
|
+
<div className="chart-wrap">
|
|
94
|
+
<BarChart rows={rows} labelField={picked.labelField} valueField={picked.valueField} />
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
<ResultTable rows={rows} />
|
|
98
|
+
</section>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
* { box-sizing: border-box; }
|
|
2
|
+
body {
|
|
3
|
+
margin: 0;
|
|
4
|
+
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
5
|
+
color: #1f2328;
|
|
6
|
+
background: #f6f7f9;
|
|
7
|
+
}
|
|
8
|
+
.page { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
|
|
9
|
+
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
10
|
+
.hint { color: #6b7280; font-size: 13px; margin: 0 0 16px; }
|
|
11
|
+
|
|
12
|
+
.panel {
|
|
13
|
+
background: #fff;
|
|
14
|
+
border: 1px solid #e5e7eb;
|
|
15
|
+
border-radius: 8px;
|
|
16
|
+
padding: 16px;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: 12px;
|
|
20
|
+
}
|
|
21
|
+
.group-by { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #374151; }
|
|
22
|
+
.group-by input { flex: 1; }
|
|
23
|
+
|
|
24
|
+
input, select {
|
|
25
|
+
padding: 6px 8px;
|
|
26
|
+
border: 1px solid #d0d7de;
|
|
27
|
+
border-radius: 6px;
|
|
28
|
+
font-size: 13px;
|
|
29
|
+
background: #fff;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.schema-filters { display: flex; flex-direction: column; gap: 8px; }
|
|
33
|
+
.filter-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
34
|
+
.filter-label { min-width: 120px; font-size: 13px; color: #374151; }
|
|
35
|
+
.filter-between { display: inline-flex; align-items: center; gap: 6px; }
|
|
36
|
+
.filter-between .sep { color: #9ca3af; }
|
|
37
|
+
.filter-value { min-width: 160px; }
|
|
38
|
+
.filter-remove { border: none; background: none; color: #b91c1c; cursor: pointer; font-size: 12px; }
|
|
39
|
+
.filter-add { align-self: flex-start; }
|
|
40
|
+
|
|
41
|
+
.btn-primary {
|
|
42
|
+
align-self: flex-start;
|
|
43
|
+
background: #2563eb;
|
|
44
|
+
color: #fff;
|
|
45
|
+
border: none;
|
|
46
|
+
border-radius: 6px;
|
|
47
|
+
padding: 8px 20px;
|
|
48
|
+
font-size: 14px;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
}
|
|
51
|
+
.btn-primary:disabled { opacity: .6; cursor: default; }
|
|
52
|
+
|
|
53
|
+
.error {
|
|
54
|
+
color: #b91c1c;
|
|
55
|
+
background: #fef2f2;
|
|
56
|
+
border: 1px solid #fecaca;
|
|
57
|
+
border-radius: 6px;
|
|
58
|
+
padding: 10px 12px;
|
|
59
|
+
margin-top: 12px;
|
|
60
|
+
font-size: 13px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.results { margin-top: 20px; }
|
|
64
|
+
.results-head { font-size: 13px; color: #374151; margin-bottom: 12px; }
|
|
65
|
+
.chart-wrap {
|
|
66
|
+
background: #fff;
|
|
67
|
+
border: 1px solid #e5e7eb;
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
padding: 12px;
|
|
70
|
+
overflow-x: auto;
|
|
71
|
+
margin-bottom: 16px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.result-table { width: 100%; border-collapse: collapse; background: #fff; font-size: 13px; }
|
|
75
|
+
.result-table th, .result-table td { border: 1px solid #e5e7eb; padding: 6px 10px; text-align: left; }
|
|
76
|
+
.result-table th { background: #f3f4f6; }
|
|
77
|
+
|
|
78
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
|
|
79
|
+
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; }
|
|
80
|
+
.card-title { font-size: 13px; color: #6b7280; }
|
|
81
|
+
.card-value { font-size: 20px; font-weight: 600; margin-top: 4px; }
|
|
82
|
+
|
|
83
|
+
.empty { color: #9ca3af; font-size: 13px; padding: 12px; }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
// app-builder G 方案:dev 时 vite 不独立跑,而是被 server/src/main.ts 的
|
|
5
|
+
// `createViteServer({ middlewareMode: true })` 当 middleware 挂进 nest,前端 HMR
|
|
6
|
+
// 和 /api 都从 nest 监听的 CLAWD_PREVIEW_PORT 出(spec 2026-06-01 §5.6)。
|
|
7
|
+
// 这个 config 仅用于:
|
|
8
|
+
// 1. `vite build` 生成 web/dist 静态资源(prod 由 nest serve)
|
|
9
|
+
// 2. 脱离 clawd 直接 `pnpm dev` 跑 vite 独立模式(本地调试)
|
|
10
|
+
const tunnelHost = process.env.CLAWD_TUNNEL_HOST ?? '';
|
|
11
|
+
const port = Number(process.env.CLAWD_PREVIEW_PORT ?? 5173);
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
plugins: [react()],
|
|
15
|
+
build: {
|
|
16
|
+
outDir: 'dist',
|
|
17
|
+
emptyOutDir: true,
|
|
18
|
+
},
|
|
19
|
+
// 隧道经 nest 主进程进来,vite middleware 自己识别 base。standalone 模式(无 tunnel)
|
|
20
|
+
// 走 base: '/' 方便本地直接打开。
|
|
21
|
+
base: tunnelHost ? `/preview/${port}/` : '/',
|
|
22
|
+
server: {
|
|
23
|
+
host: '127.0.0.1',
|
|
24
|
+
port,
|
|
25
|
+
strictPort: true,
|
|
26
|
+
hmr: tunnelHost
|
|
27
|
+
? { protocol: 'wss', clientPort: 443, path: `/preview/${port}/`, host: tunnelHost }
|
|
28
|
+
: undefined,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
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 作用域)。
|
|
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
|
|
9
|
+
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
|
+
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"
|
|
23
|
+
|
|
24
|
+
# 可选占位符显式兜底(未填时通用扫描需「已定义」才放行)
|
|
25
|
+
export DATACLAW_AS_UNION_ID="${DATACLAW_AS_UNION_ID:-}"
|
|
@@ -115,5 +115,3 @@ 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.
|
|
3
|
+
"version": "0.2.143-beta.301.814d293",
|
|
4
4
|
"description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@clawos/clawd-protocol": "workspace:*",
|
|
30
|
-
"@clawos/log-client": "workspace:*",
|
|
31
30
|
"@types/diff": "^7.0.2",
|
|
32
31
|
"@types/node": "^22.0.0",
|
|
33
32
|
"@types/uuid": "^10.0.0",
|
|
@@ -49,10 +48,8 @@
|
|
|
49
48
|
},
|
|
50
49
|
"dependencies": {
|
|
51
50
|
"@lydell/node-pty": "1.2.0-beta.12",
|
|
52
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
53
51
|
"@xterm/addon-serialize": "^0.14.0",
|
|
54
52
|
"@xterm/headless": "^6.0.0",
|
|
55
|
-
"jszip": "^3.10.1"
|
|
56
|
-
"undici": "^6"
|
|
53
|
+
"jszip": "^3.10.1"
|
|
57
54
|
}
|
|
58
55
|
}
|