feat: add Z12/Z13 bind APIs, stock import, and sync docs
Enable auto default sync channels on agent activate, bind-code/phone confirm flows, publish ALTER, and align admin/yuheng docs with the production bind path.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Card, Space, Table, Tag, Typography, message } from "antd";
|
||||
import { ReloadOutlined } from "@ant-design/icons";
|
||||
import { Session, listApps } from "./api";
|
||||
import { App as AntApp, Button, Card, Space, Table, Tag, Typography, message } from "antd";
|
||||
import { ReloadOutlined, ImportOutlined } from "@ant-design/icons";
|
||||
import { Session, ensureAppsImport, listApps } from "./api";
|
||||
|
||||
type Mod = {
|
||||
app_id: string;
|
||||
@@ -21,9 +21,11 @@ export function ModulesPage(props: {
|
||||
onContinueDraft: (slug: string) => void;
|
||||
}) {
|
||||
const { session, onOpenPublished, onContinueDraft } = props;
|
||||
const { modal } = AntApp.useApp();
|
||||
const [items, setItems] = useState<Mod[]>([]);
|
||||
const [scope, setScope] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [ensuring, setEnsuring] = useState(false);
|
||||
|
||||
async function refresh() {
|
||||
setLoading(true);
|
||||
@@ -38,6 +40,27 @@ export function ModulesPage(props: {
|
||||
}
|
||||
}
|
||||
|
||||
function onEnsureImport() {
|
||||
modal.confirm({
|
||||
title: "一键开启全部业务表导入?",
|
||||
content: "将为本公司所有已发布模块补齐蓝图 import/export(无需逐个再发布)。只读表不会改动。",
|
||||
okText: "开启",
|
||||
onOk: async () => {
|
||||
setEnsuring(true);
|
||||
try {
|
||||
const res = await ensureAppsImport(session, false);
|
||||
message.success(res.message || `已更新 ${res.updated} 个模块`);
|
||||
await refresh();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || String(e));
|
||||
throw e;
|
||||
} finally {
|
||||
setEnsuring(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -54,15 +77,27 @@ export function ModulesPage(props: {
|
||||
<Card
|
||||
title="模块管理"
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={refresh} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Space>
|
||||
<Button icon={<ImportOutlined />} loading={ensuring} onClick={() => onEnsureImport()}>
|
||||
一键开启导入
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={refresh} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
|
||||
管理账号(如 owner)可查看本租户<strong>全部模块</strong>,含<strong>在建</strong>与已发布。
|
||||
智能体默认不限制模块(可自由发布自建 slug);仅当配置了白名单时才受限。
|
||||
{scope === "all" ? " 当前范围:管理全部。" : scope === "open" ? " 当前范围:智能体开放。" : scope === "granted" ? " 当前范围:白名单。" : null}
|
||||
「一键开启导入」修复旧模块因蓝图缺 import 导致的导入 400(Z9e)。
|
||||
{scope === "all"
|
||||
? " 当前范围:管理全部。"
|
||||
: scope === "open"
|
||||
? " 当前范围:智能体开放。"
|
||||
: scope === "granted"
|
||||
? " 当前范围:白名单。"
|
||||
: null}
|
||||
</Typography.Paragraph>
|
||||
<Table
|
||||
rowKey="app_id"
|
||||
|
||||
@@ -415,6 +415,16 @@ export function SyncPage(props: {
|
||||
dataSource={visibleChannels}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: "通道 ID",
|
||||
dataIndex: "id",
|
||||
width: 280,
|
||||
render: (id: string) => (
|
||||
<Typography.Text copyable={{ text: id }} style={{ fontSize: 12 }}>
|
||||
{id}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{ title: "名称", dataIndex: "name" },
|
||||
{
|
||||
title: "方向",
|
||||
@@ -448,12 +458,24 @@ export function SyncPage(props: {
|
||||
title: "智能体/模块",
|
||||
render: (_: unknown, r: SyncChannel) => {
|
||||
const ag = agents.find((a) => a.agent_id === r.agent_id);
|
||||
const app = appOptions.find((o) => o.value === r.app_slug);
|
||||
const moduleLabel = r.app_slug
|
||||
? app
|
||||
? `${(app.label.split(" (")[0] || app.label).trim()}`
|
||||
: r.app_slug
|
||||
: "未绑模块";
|
||||
return (
|
||||
<span>
|
||||
{ag ? ag.name : r.agent_id ? `#${r.agent_id}` : "—"}
|
||||
<br />
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{r.app_slug || "未绑模块"}
|
||||
{moduleLabel}
|
||||
{r.app_slug && app ? (
|
||||
<>
|
||||
<br />
|
||||
<span style={{ opacity: 0.75 }}>{r.app_slug}</span>
|
||||
</>
|
||||
) : null}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
);
|
||||
@@ -640,13 +662,15 @@ export function SyncPage(props: {
|
||||
</Typography.Text>
|
||||
);
|
||||
}
|
||||
const tables = (ch.remote?.tables || []).join(", ") || "(按 Binding 任意表)";
|
||||
const tables = (ch.remote?.tables || []).join(", ");
|
||||
return (
|
||||
<span>
|
||||
{ch.name || id}
|
||||
<br />
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
|
||||
通道表白名单配置:{tables}
|
||||
{tables
|
||||
? `策略表(参考,非整库限制):${tables}`
|
||||
: "整库同步中(Binding 任意表可 push)"}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
);
|
||||
@@ -869,9 +893,9 @@ export function SyncPage(props: {
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["local", "tables"]}
|
||||
label="表(逗号分隔)"
|
||||
label="策略表(参考)"
|
||||
rules={[{ required: true }]}
|
||||
extra="通道表名单仅作提示;用户 JWT + Binding 下任意表可 push"
|
||||
extra="Z4:不按此名单拒收;用户 JWT + Binding 下任意表可 push。此处仅运维提示。"
|
||||
>
|
||||
<Input placeholder="article,order" />
|
||||
</Form.Item>
|
||||
@@ -901,9 +925,9 @@ export function SyncPage(props: {
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["remote", "tables"]}
|
||||
label="表(逗号分隔)"
|
||||
label="策略表(参考)"
|
||||
rules={[{ required: true }]}
|
||||
extra="与本地提示表一致即可;验同步请用「查看线上表」"
|
||||
extra="非整库限制;与本地提示一致即可。验同步请用「查看线上表」"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
@@ -296,6 +296,25 @@ export async function listApps(session: Session) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Z9e:为本租户已发布模块补齐 import/export(一键开启导入) */
|
||||
export async function ensureAppsImport(session: Session, dryRun = false) {
|
||||
const q = dryRun ? "?dry_run=1" : "";
|
||||
const res = await apiFetch(`${PLATFORM}/api/v1/admin/apps/ensure-import${q}`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||
});
|
||||
const data = await readJson(res);
|
||||
throwIfBad(res, data, "一键开启导入失败");
|
||||
return data as {
|
||||
scanned: number;
|
||||
updated: number;
|
||||
skipped: number;
|
||||
dry_run: boolean;
|
||||
updated_slugs?: string[];
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveDraft(session: Session, slug: string, blueprint: any) {
|
||||
const res = await apiFetch(`${PLATFORM}/api/v1/apps/${slug}/draft`, {
|
||||
method: "PUT",
|
||||
|
||||
Reference in New Issue
Block a user