feat: Page Data dev loop, adm Code Run UI, and customer order demo
Add verify→Aider→OpenHands dev loop, memindadm config/history UI, and john4 customer-order Page Data demo with scenario + verification scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,8 @@ import { ReviewPage } from './pages/ReviewPage';
|
||||
import { SummaryPage } from './pages/admin/SummaryPage';
|
||||
import { UsersPage } from './pages/admin/UsersPage';
|
||||
import { BillingPage } from './pages/admin/BillingPage';
|
||||
import { AgentCodeRunPage } from './pages/admin/AgentCodeRunPage';
|
||||
import { WechatPage } from './pages/admin/WechatPage';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
@@ -42,6 +44,7 @@ export function App() {
|
||||
<Route index element={<SummaryPage />} />
|
||||
<Route path="users" element={<UsersPage />} />
|
||||
<Route path="billing" element={<BillingPage />} />
|
||||
<Route path="agent-code-run" element={<AgentCodeRunPage />} />
|
||||
<Route path="wechat" element={<WechatPage />} />
|
||||
<Route path="*" element={<Navigate to="/admin" replace />} />
|
||||
</Route>
|
||||
|
||||
@@ -324,3 +324,81 @@ export async function resumeWechatDigest(id: string) {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Agent Code Run ───────────────────────────────────────────────────────────
|
||||
|
||||
export type AgentCodeRunConfigShape = {
|
||||
codeRun: {
|
||||
enabled: boolean;
|
||||
clientEnabled: boolean;
|
||||
generalAutodetect: boolean;
|
||||
requireValidation: boolean;
|
||||
userAllowlist: string[];
|
||||
taskTypeAllowlist: string[];
|
||||
};
|
||||
pageDataDev: {
|
||||
autodetect: boolean;
|
||||
};
|
||||
meta: {
|
||||
notes: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AgentCodeRunAdminConfig = {
|
||||
config: AgentCodeRunConfigShape;
|
||||
updatedAt: number | null;
|
||||
updatedBy: string | null;
|
||||
source: string;
|
||||
envOverrideActive: boolean;
|
||||
};
|
||||
|
||||
export type AgentCodeRunRuntimeState = {
|
||||
source: string;
|
||||
updatedAt: number | null;
|
||||
updatedBy: string | null;
|
||||
config: AgentCodeRunConfigShape;
|
||||
policy: {
|
||||
source: string;
|
||||
enabled: boolean;
|
||||
clientEnabled: boolean;
|
||||
generalAutodetect: boolean;
|
||||
requireValidation: boolean;
|
||||
pageDataDevAutodetect: boolean;
|
||||
userAllowlist: string[];
|
||||
taskTypeAllowlist: string[];
|
||||
};
|
||||
envOverrideActive: boolean;
|
||||
};
|
||||
|
||||
export async function fetchAgentCodeRunConfig() {
|
||||
return adminFetch<AgentCodeRunAdminConfig>('/admin-api/agent-code-run/config');
|
||||
}
|
||||
|
||||
export async function fetchAgentCodeRunRuntime() {
|
||||
return adminFetch<AgentCodeRunRuntimeState>('/admin-api/agent-code-run/runtime');
|
||||
}
|
||||
|
||||
export async function patchAgentCodeRunConfig(body: { config: AgentCodeRunConfigShape }) {
|
||||
return adminFetch<AgentCodeRunAdminConfig>('/admin-api/agent-code-run/config', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
export type AgentCodeRunHistoryRow = {
|
||||
id: string;
|
||||
userId: string;
|
||||
requestId: string;
|
||||
status: string;
|
||||
taskType: string;
|
||||
executor: string | null;
|
||||
errorMessage: string | null;
|
||||
createdAt: number | null;
|
||||
updatedAt: number | null;
|
||||
};
|
||||
|
||||
export async function fetchAgentCodeRunHistory(limit = 50) {
|
||||
return adminFetch<{ runs: AgentCodeRunHistoryRow[]; limit: number }>(
|
||||
`/admin-api/agent-code-run/runs?limit=${limit}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const links = [
|
||||
{ to: '/admin/users', label: '用户管理' },
|
||||
{ to: '/admin/billing', label: '账单记录' },
|
||||
{ to: '/admin/wechat', label: '服务号管理' },
|
||||
{ to: '/admin/agent-code-run', label: 'Code Run' },
|
||||
];
|
||||
|
||||
export function AdminLayout() {
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
fetchAgentCodeRunConfig,
|
||||
fetchAgentCodeRunHistory,
|
||||
fetchAgentCodeRunRuntime,
|
||||
patchAgentCodeRunConfig,
|
||||
type AgentCodeRunAdminConfig,
|
||||
type AgentCodeRunHistoryRow,
|
||||
type AgentCodeRunRuntimeState,
|
||||
} from '../../api/admin';
|
||||
|
||||
type FormState = {
|
||||
enabled: boolean;
|
||||
clientEnabled: boolean;
|
||||
generalAutodetect: boolean;
|
||||
requireValidation: boolean;
|
||||
pageDataDevAutodetect: boolean;
|
||||
userAllowlist: string;
|
||||
taskTypeAllowlist: string;
|
||||
notes: string;
|
||||
};
|
||||
|
||||
function listToText(values: string[] | undefined) {
|
||||
return (values ?? []).join(', ');
|
||||
}
|
||||
|
||||
function textToList(value: string) {
|
||||
return value
|
||||
.split(/[,\n]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function configToForm(config: AgentCodeRunAdminConfig['config']): FormState {
|
||||
return {
|
||||
enabled: Boolean(config.codeRun.enabled),
|
||||
clientEnabled: Boolean(config.codeRun.clientEnabled),
|
||||
generalAutodetect: Boolean(config.codeRun.generalAutodetect),
|
||||
requireValidation: Boolean(config.codeRun.requireValidation),
|
||||
pageDataDevAutodetect: Boolean(config.pageDataDev.autodetect),
|
||||
userAllowlist: listToText(config.codeRun.userAllowlist),
|
||||
taskTypeAllowlist: listToText(config.codeRun.taskTypeAllowlist),
|
||||
notes: config.meta?.notes ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function formToPatch(form: FormState) {
|
||||
return {
|
||||
config: {
|
||||
codeRun: {
|
||||
enabled: form.enabled,
|
||||
clientEnabled: form.clientEnabled,
|
||||
generalAutodetect: form.generalAutodetect,
|
||||
requireValidation: form.requireValidation,
|
||||
userAllowlist: textToList(form.userAllowlist),
|
||||
taskTypeAllowlist: textToList(form.taskTypeAllowlist),
|
||||
},
|
||||
pageDataDev: {
|
||||
autodetect: form.pageDataDevAutodetect,
|
||||
},
|
||||
meta: {
|
||||
notes: form.notes,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function formatTime(value: number | null | undefined) {
|
||||
if (!value) return '—';
|
||||
return new Date(value).toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
function ToggleRow({
|
||||
label,
|
||||
hint,
|
||||
checked,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
label: string;
|
||||
hint?: string;
|
||||
checked: boolean;
|
||||
onChange: (next: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<label style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 12, alignItems: 'start' }}>
|
||||
<span>
|
||||
<strong>{label}</strong>
|
||||
{hint ? <p style={{ margin: '4px 0 0', color: '#68716c', fontSize: 13 }}>{hint}</p> : null}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.checked)}
|
||||
style={{ width: 'auto', marginTop: 4 }}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentCodeRunPage() {
|
||||
const [configState, setConfigState] = useState<AgentCodeRunAdminConfig | null>(null);
|
||||
const [runtimeState, setRuntimeState] = useState<AgentCodeRunRuntimeState | null>(null);
|
||||
const [runHistory, setRunHistory] = useState<AgentCodeRunHistoryRow[]>([]);
|
||||
const [form, setForm] = useState<FormState | null>(null);
|
||||
const [jsonDraft, setJsonDraft] = useState('');
|
||||
const [useJson, setUseJson] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const locked = Boolean(configState?.envOverrideActive);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [config, runtime, history] = await Promise.all([
|
||||
fetchAgentCodeRunConfig(),
|
||||
fetchAgentCodeRunRuntime(),
|
||||
fetchAgentCodeRunHistory(30),
|
||||
]);
|
||||
setConfigState(config);
|
||||
setRuntimeState(runtime);
|
||||
setRunHistory(history.runs ?? []);
|
||||
setForm(configToForm(config.config));
|
||||
setJsonDraft(JSON.stringify(config.config, null, 2));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
const effectivePolicy = runtimeState?.policy;
|
||||
|
||||
const previewJson = useMemo(() => {
|
||||
if (!form) return '';
|
||||
return JSON.stringify(formToPatch(form).config, null, 2);
|
||||
}, [form]);
|
||||
|
||||
async function handleSave() {
|
||||
if (!form) return;
|
||||
setSaving(true);
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
try {
|
||||
let patch: ReturnType<typeof formToPatch>;
|
||||
if (useJson) {
|
||||
const parsed = JSON.parse(jsonDraft) as AgentCodeRunAdminConfig['config'];
|
||||
patch = { config: parsed };
|
||||
} else {
|
||||
patch = formToPatch(form);
|
||||
}
|
||||
const saved = await patchAgentCodeRunConfig(patch);
|
||||
setConfigState(saved);
|
||||
setForm(configToForm(saved.config));
|
||||
setJsonDraft(JSON.stringify(saved.config, null, 2));
|
||||
setMessage('配置已保存');
|
||||
const runtime = await fetchAgentCodeRunRuntime();
|
||||
setRuntimeState(runtime);
|
||||
const history = await fetchAgentCodeRunHistory(30);
|
||||
setRunHistory(history.runs ?? []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <p>加载中…</p>;
|
||||
if (error && !form) return <p className="alert">{error}</p>;
|
||||
if (!form || !configState) return <p className="alert">配置不可用</p>;
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="card">
|
||||
<h2 style={{ marginTop: 0 }}>Agent Code Run & Page Data Dev</h2>
|
||||
<p style={{ color: '#68716c', marginTop: 0 }}>
|
||||
控制 code run 灰度、H5 客户端开关与 Page Data 开发修复 autodetect。Worker 拓扑仍由 env 管理。
|
||||
</p>
|
||||
<div style={{ display: 'grid', gap: 8, fontSize: 13 }}>
|
||||
<p style={{ margin: 0 }}>
|
||||
当前来源:<strong>{configState.source}</strong>
|
||||
{configState.envOverrideActive ? (
|
||||
<span className="warn"> · MEMIND_CODE_RUN_POLICY_SOURCE=env 已锁定后台修改</span>
|
||||
) : null}
|
||||
</p>
|
||||
<p style={{ margin: 0 }}>更新时间:{formatTime(configState.updatedAt)}</p>
|
||||
{effectivePolicy ? (
|
||||
<p style={{ margin: 0 }}>
|
||||
有效策略:server={effectivePolicy.enabled ? '开' : '关'} · client=
|
||||
{effectivePolicy.clientEnabled ? '开' : '关'} · pageDataDev=
|
||||
{effectivePolicy.pageDataDevAutodetect ? '开' : '关'}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{locked ? (
|
||||
<div className="card warn">
|
||||
当前环境启用了 <code>MEMIND_CODE_RUN_POLICY_SOURCE=env</code>,后台无法写入 DB。请改 env 或去掉该变量后再保存。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="card" style={{ display: 'grid', gap: 16 }}>
|
||||
<h3 style={{ margin: 0 }}>开关</h3>
|
||||
<ToggleRow
|
||||
label="Code Run 总开关(服务端)"
|
||||
hint="POST /agent/runs 的 tool_mode=code 门禁"
|
||||
checked={form.enabled}
|
||||
disabled={locked}
|
||||
onChange={(enabled) => setForm((current) => (current ? { ...current, enabled } : current))}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="H5 客户端启用"
|
||||
hint="/auth/status.agentCodeRun.codeRun.enabled"
|
||||
checked={form.clientEnabled}
|
||||
disabled={locked}
|
||||
onChange={(clientEnabled) => setForm((current) => (current ? { ...current, clientEnabled } : current))}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="Page Data Dev Autodetect"
|
||||
hint="聊天识别「修问卷 / verify 失败」→ page_data_dev"
|
||||
checked={form.pageDataDevAutodetect}
|
||||
disabled={locked}
|
||||
onChange={(pageDataDevAutodetect) =>
|
||||
setForm((current) => (current ? { ...current, pageDataDevAutodetect } : current))
|
||||
}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="通用 Code Autodetect"
|
||||
hint="普通聊天文本识别代码任务(默认建议关闭)"
|
||||
checked={form.generalAutodetect}
|
||||
disabled={locked}
|
||||
onChange={(generalAutodetect) =>
|
||||
setForm((current) => (current ? { ...current, generalAutodetect } : current))
|
||||
}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="强制 Validation"
|
||||
hint="code run 必须带 expectedFiles / receipt"
|
||||
checked={form.requireValidation}
|
||||
disabled={locked}
|
||||
onChange={(requireValidation) =>
|
||||
setForm((current) => (current ? { ...current, requireValidation } : current))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ display: 'grid', gap: 12 }}>
|
||||
<h3 style={{ margin: 0 }}>白名单</h3>
|
||||
<label style={{ display: 'grid', gap: 6 }}>
|
||||
<strong>用户 UUID(逗号或换行分隔,留空=全部)</strong>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={form.userAllowlist}
|
||||
disabled={locked}
|
||||
onChange={(event) =>
|
||||
setForm((current) => (current ? { ...current, userAllowlist: event.target.value } : current))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label style={{ display: 'grid', gap: 6 }}>
|
||||
<strong>taskType 允许列表</strong>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={form.taskTypeAllowlist}
|
||||
disabled={locked}
|
||||
onChange={(event) =>
|
||||
setForm((current) => (current ? { ...current, taskTypeAllowlist: event.target.value } : current))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label style={{ display: 'grid', gap: 6 }}>
|
||||
<strong>备注</strong>
|
||||
<input
|
||||
value={form.notes}
|
||||
disabled={locked}
|
||||
onChange={(event) => setForm((current) => (current ? { ...current, notes: event.target.value } : current))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ display: 'grid', gap: 12 }}>
|
||||
<label style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useJson}
|
||||
disabled={locked}
|
||||
onChange={(event) => setUseJson(event.target.checked)}
|
||||
style={{ width: 'auto' }}
|
||||
/>
|
||||
<strong>高级:直接编辑 JSON config</strong>
|
||||
</label>
|
||||
{useJson ? (
|
||||
<textarea
|
||||
rows={16}
|
||||
value={jsonDraft}
|
||||
disabled={locked}
|
||||
onChange={(event) => setJsonDraft(event.target.value)}
|
||||
style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12 }}
|
||||
/>
|
||||
) : (
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
background: '#f8f4ea',
|
||||
overflow: 'auto',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{previewJson}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<button className="btn" type="button" disabled={saving || locked} onClick={() => void handleSave()}>
|
||||
{saving ? '保存中…' : '保存配置'}
|
||||
</button>
|
||||
<button className="btn secondary" type="button" disabled={loading} onClick={() => void load()}>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{message ? <p style={{ color: '#2f6f57' }}>{message}</p> : null}
|
||||
{error ? <p className="alert">{error}</p> : null}
|
||||
|
||||
{runtimeState ? (
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>运行时只读</h3>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
background: '#f8f4ea',
|
||||
overflow: 'auto',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(runtimeState, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>Page Data Dev Run 历史</h3>
|
||||
{runHistory.length === 0 ? (
|
||||
<p style={{ color: '#68716c', margin: 0 }}>暂无 page_data_dev / page_data_dev_complex 记录</p>
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ textAlign: 'left', padding: '8px 6px', color: '#68716c' }}>时间</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px 6px', color: '#68716c' }}>用户</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px 6px', color: '#68716c' }}>taskType</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px 6px', color: '#68716c' }}>executor</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px 6px', color: '#68716c' }}>状态</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px 6px', color: '#68716c' }}>requestId</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runHistory.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td style={{ padding: '8px 6px', borderTop: '1px solid #eee7da' }}>{formatTime(row.updatedAt)}</td>
|
||||
<td style={{ padding: '8px 6px', borderTop: '1px solid #eee7da', fontFamily: 'monospace', fontSize: 11 }}>
|
||||
{row.userId.slice(0, 8)}…
|
||||
</td>
|
||||
<td style={{ padding: '8px 6px', borderTop: '1px solid #eee7da' }}>{row.taskType}</td>
|
||||
<td style={{ padding: '8px 6px', borderTop: '1px solid #eee7da' }}>{row.executor ?? '—'}</td>
|
||||
<td style={{ padding: '8px 6px', borderTop: '1px solid #eee7da' }}>{row.status}</td>
|
||||
<td style={{ padding: '8px 6px', borderTop: '1px solid #eee7da', fontFamily: 'monospace', fontSize: 11 }}>
|
||||
{row.requestId}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -65,6 +65,9 @@ export function SummaryPage() {
|
||||
<Link className="btn" to="/admin/wechat" style={{ textAlign: 'center', textDecoration: 'none' }}>
|
||||
进入通知推送
|
||||
</Link>
|
||||
<Link className="btn secondary" to="/admin/agent-code-run" style={{ textAlign: 'center', textDecoration: 'none' }}>
|
||||
Code Run 配置
|
||||
</Link>
|
||||
<Link
|
||||
className="btn secondary"
|
||||
to="/admin/users"
|
||||
|
||||
Reference in New Issue
Block a user