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:
whm
2026-08-05 11:47:20 +08:00
parent b04b180d30
commit cb56e6847e
31 changed files with 1882 additions and 91 deletions

View File

@@ -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 400Z9e
{scope === "all"
? " 当前范围:管理全部。"
: scope === "open"
? " 当前范围:智能体开放。"
: scope === "granted"
? " 当前范围:白名单。"
: null}
</Typography.Paragraph>
<Table
rowKey="app_id"

View File

@@ -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>

View File

@@ -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",