feat: Z36 host_meta-only publish + Z37 SyncPage LWW UX
Allow display-name-only publish via host_meta; stop treating company conflicts 403 as a fault and ship rebuilt web dist. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -229,6 +229,9 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
|
||||
bp.Apis.BasePath = "/api/v1/apps/" + slug
|
||||
mergeRes, err = blueprint.MergeInto(bp, incoming)
|
||||
if err != nil {
|
||||
if isNothingNewToPublish(err) {
|
||||
return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
publishMode = "pages_added"
|
||||
@@ -249,6 +252,9 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
|
||||
bp.Apis.BasePath = "/api/v1/apps/" + slug
|
||||
mergeRes, err = blueprint.MergeInto(bp, incoming)
|
||||
if err != nil {
|
||||
if isNothingNewToPublish(err) {
|
||||
return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
publishMode = "pages_added"
|
||||
@@ -258,6 +264,13 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
|
||||
}
|
||||
}
|
||||
|
||||
// Z36:有蓝图增量时也把 host_meta.module_name 落到蓝图/模块显示名
|
||||
if req.HostMeta != nil {
|
||||
if n := strings.TrimSpace(req.HostMeta.ModuleName); n != "" {
|
||||
bp.Meta.Name = n
|
||||
}
|
||||
}
|
||||
|
||||
bp.EnsureDefaultImportExport()
|
||||
if err := bp.Validate(slug); err != nil {
|
||||
return nil, err
|
||||
@@ -483,6 +496,112 @@ func (l *PublishLogic) audit(action, detail string) error {
|
||||
return l.svcCtx.Audit.Log(l.ctx, authx.TenantID(l.ctx), authx.UserID(l.ctx), action, detail)
|
||||
}
|
||||
|
||||
func isNothingNewToPublish(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "nothing new to publish")
|
||||
}
|
||||
|
||||
// handleNoBlueprintDelta Z36:merge 无增量时,有 module_name 则仅更新 host_meta;否则中文业务错误。
|
||||
func (l *PublishLogic) handleNoBlueprintDelta(
|
||||
existing *meta.AppRecord,
|
||||
req *types.PublishReq,
|
||||
slug string,
|
||||
tenantID, userID int64,
|
||||
) (*types.PublishResp, error) {
|
||||
name := ""
|
||||
if req.HostMeta != nil {
|
||||
name = strings.TrimSpace(req.HostMeta.ModuleName)
|
||||
}
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("没有可发布的蓝图增量(页面/实体/接口未变化)。若仅修改模块显示名,请在 host_meta.module_name 传入新名称后重试 [NO_BLUEPRINT_DELTA]")
|
||||
}
|
||||
return l.publishHostMetaOnly(existing, req, slug, tenantID, userID, name)
|
||||
}
|
||||
|
||||
// publishHostMetaOnly 无蓝图增量时只更新模块显示名并重签胶囊,不跑 DDL。
|
||||
func (l *PublishLogic) publishHostMetaOnly(
|
||||
existing *meta.AppRecord,
|
||||
req *types.PublishReq,
|
||||
slug string,
|
||||
tenantID, userID int64,
|
||||
moduleName string,
|
||||
) (*types.PublishResp, error) {
|
||||
if existing == nil || existing.Blueprint == nil {
|
||||
return nil, fmt.Errorf("existing app has no blueprint")
|
||||
}
|
||||
bp := existing.Blueprint
|
||||
bp.Meta.Name = moduleName
|
||||
bp.Meta.Slug = slug
|
||||
if bp.Apis.BasePath == "" {
|
||||
bp.Apis.BasePath = "/api/v1/apps/" + slug
|
||||
}
|
||||
|
||||
rec := existing
|
||||
rec.Name = moduleName
|
||||
rec.Blueprint = bp
|
||||
rec.UpdatedAt = time.Now().UTC()
|
||||
if rec.Status == "" {
|
||||
rec.Status = meta.StatusPublished
|
||||
}
|
||||
if err := l.svcCtx.Meta.Save(l.ctx, rec); err != nil {
|
||||
return nil, fmt.Errorf("meta save: %w", err)
|
||||
}
|
||||
_ = l.audit("publish.host_meta", audit.DetailJSON(map[string]any{
|
||||
"slug": slug, "module_name": moduleName, "user_id": userID, "publish_mode": "host_meta_updated",
|
||||
}))
|
||||
|
||||
ownerID := authx.AgentID(l.ctx)
|
||||
if ownerID <= 0 {
|
||||
ownerID = userID
|
||||
}
|
||||
secret := l.svcCtx.Config.Agent.CapsuleSecret
|
||||
if secret == "" {
|
||||
secret = l.svcCtx.JWT.AccessSecret
|
||||
}
|
||||
accessPath := ""
|
||||
accessURL := ""
|
||||
if secret != "" && ownerID > 0 {
|
||||
_, filePath, err := agentcap.SealModulePath(secret, tenantID, ownerID, slug)
|
||||
if err == nil {
|
||||
accessPath = filePath
|
||||
base := strings.TrimRight(l.svcCtx.Config.PublicBaseURL, "/")
|
||||
if req.HostMeta != nil && strings.TrimSpace(req.HostMeta.HostBaseURL) != "" {
|
||||
accessURL = strings.TrimRight(strings.TrimSpace(req.HostMeta.HostBaseURL), "/")
|
||||
} else if base != "" {
|
||||
accessURL = base + "/api/v1/public/" + filePath + "/blueprint"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishStyle := ""
|
||||
if req.HostMeta != nil {
|
||||
publishStyle = strings.TrimSpace(req.HostMeta.PublishStyle)
|
||||
}
|
||||
if publishStyle == "" {
|
||||
publishStyle = "immediate"
|
||||
}
|
||||
|
||||
endpoints := rec.Endpoints
|
||||
if len(endpoints) == 0 {
|
||||
endpoints = buildEndpoints(bp)
|
||||
}
|
||||
return &types.PublishResp{
|
||||
AppID: rec.AppID,
|
||||
Slug: slug,
|
||||
SchemaName: rec.SchemaName,
|
||||
DatabaseName: rec.DatabaseName,
|
||||
Status: string(rec.Status),
|
||||
Endpoints: endpoints,
|
||||
MemoryMode: l.svcCtx.MemoryMode,
|
||||
PublishMode: "host_meta_updated",
|
||||
ModuleName: moduleName,
|
||||
PublishStyle: publishStyle,
|
||||
AccessPath: accessPath,
|
||||
AccessURL: accessURL,
|
||||
PublishedAt: rec.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
OwnerID: ownerID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildEndpoints(bp *blueprint.Blueprint) []string {
|
||||
base := strings.TrimRight(bp.Apis.BasePath, "/")
|
||||
if base == "" {
|
||||
|
||||
@@ -29,7 +29,7 @@ type PublishResp struct {
|
||||
Endpoints []string `json:"endpoints"`
|
||||
DDL []string `json:"ddl,omitempty"`
|
||||
MemoryMode bool `json:"memory_mode"`
|
||||
PublishMode string `json:"publish_mode"` // created | pages_added | replaced
|
||||
PublishMode string `json:"publish_mode"` // created | pages_added | replaced | host_meta_updated
|
||||
AddedPages []string `json:"added_pages,omitempty"`
|
||||
AddedEntities []string `json:"added_entities,omitempty"`
|
||||
AddedResources []string `json:"added_resources,omitempty"`
|
||||
|
||||
379
web/dist/assets/index-C41vYD6l.js
vendored
Normal file
379
web/dist/assets/index-C41vYD6l.js
vendored
Normal file
File diff suppressed because one or more lines are too long
379
web/dist/assets/index-WRB2YG1b.js
vendored
379
web/dist/assets/index-WRB2YG1b.js
vendored
File diff suppressed because one or more lines are too long
2
web/dist/index.html
vendored
2
web/dist/index.html
vendored
@@ -7,7 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<script type="module" crossorigin src="/assets/index-WRB2YG1b.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C41vYD6l.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CtqUfnO-.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -313,7 +313,7 @@ export function PlatformTenantsPage(props: {
|
||||
next.tenantName = r.name;
|
||||
onSession?.(next);
|
||||
setInfo(`已打开「${r.name}」管理视图(身份仍是平台超管)`);
|
||||
message.success(`已打开「${r.name}」`);
|
||||
message.success(`已打开「${r.name}」管理视图(身份仍是平台超管)`);
|
||||
onEnteredCompany?.();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || String(e));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
App as AntApp,
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -486,6 +487,13 @@ export function SyncPage(props: {
|
||||
线上库 A 推荐 <Typography.Text code>postgres</Typography.Text>;多服务器每库一条通道。
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message="冲突已自动按 LWW 处理;覆盖审计请到「平台超管 · 租户/公司 · LWW 覆盖日志」查看。公司侧无冲突台。"
|
||||
/>
|
||||
|
||||
<Space style={{ marginBottom: 12 }} wrap>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建通道
|
||||
@@ -511,6 +519,11 @@ export function SyncPage(props: {
|
||||
loading={loading}
|
||||
dataSource={visibleChannels}
|
||||
pagination={false}
|
||||
locale={{
|
||||
emptyText: filterAgentId
|
||||
? "该智能体暂无同步通道"
|
||||
: "该公司暂无同步通道(未绑定智能体或尚未创建通道)",
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
title: "通道 ID",
|
||||
@@ -908,7 +921,7 @@ export function SyncPage(props: {
|
||||
/>
|
||||
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: 16 }}>
|
||||
冲突已改为自动 LWW;覆盖审计仅平台超级管理员可查。「同步修复」有最小间隔限流(默认 5 分钟)。
|
||||
「同步修复」有最小间隔限流(默认 5 分钟)。
|
||||
↑ 推送次数 = agent push 成功次数,不是业务表数量。
|
||||
本地有、线上没有的表(如尚未 push 的业务表)属正常,不是 Binding 映射错了。
|
||||
Binding 标 <Typography.Text code>shared=true</Typography.Text> 为公司共享库(仅管理员可设)。
|
||||
|
||||
@@ -1017,13 +1017,16 @@ export async function stopSyncChannel(session: Session, id: string) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/** @deprecated 公司侧已 403;请用 listPlatformLwwOverrides */
|
||||
/** @deprecated 公司侧已 403;请用 listPlatformLwwOverrides。预期 403 静默返回空,勿当故障。 */
|
||||
export async function listSyncConflicts(session: Session, unresolved = true) {
|
||||
const q = unresolved ? "?unresolved=1" : "?unresolved=0";
|
||||
const res = await apiFetch(`/api/v1/admin/sync/conflicts${q}`, {
|
||||
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||
});
|
||||
const data = await readJson(res);
|
||||
if (res.status === 403) {
|
||||
return { items: [] as SyncConflict[] };
|
||||
}
|
||||
throwIfBad(res, data, "list conflicts failed (公司侧已禁用,请用超管 LWW 审计)");
|
||||
return data as { items: SyncConflict[] };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 联调后修改意见 · 宇恒松离线(形态 B)
|
||||
|
||||
> 初稿:2026-08-01 · 修订至 **2026-08-07**(Z34b 智建已落实:restore-by-host)
|
||||
> 初稿:2026-08-01 · 修订至 **2026-08-07**(+Z37 SyncPage LWW 403 体验)
|
||||
> 焦点:**§0.2**(按负责方);**改代码前须先写入本意见**(见 §0.0)
|
||||
> 来源:宇恒 `yuhengyihao_client` ↔ 智建(生产 `aisite.yuxindazhineng.com`)
|
||||
> 依据:`松离线-dbsync方案-最终版.md`、`宇恒-松离线数据同步使用文档.md`
|
||||
@@ -31,6 +31,8 @@
|
||||
| **Z24 已绑定勿 force 清落点** | **宇恒已改完;智建无需** | 启动自动引导绑定时禁止 `force_rebind` 清 CHANNEL;仅用户点「换绑」才清(§5.22) |
|
||||
| **Z34 / Z34b 换机恢复** | **宇恒半程 + 智建已落实** | 有手机静默恢复(Z34);无手机 `POST …/restore-by-host`(Z34b · §5.32) |
|
||||
| **Z35 库名非法中文** | **智建已落实(须生产 pull)** | 绑定/AttachSyncBind/heal 纠正;publish 拒非法名;§5.33;**宇恒勿改** |
|
||||
| **Z36 编辑发布无增量** | **智建已落实(须生产 pull)** | 无增量 + `host_meta.module_name` → `host_meta_updated`;否则中文 `[NO_BLUEPRINT_DELTA]`(§5.34) |
|
||||
| **Z37 SyncPage LWW 403 吓人** | **智建已落实(须生产 pull)** | SyncPage 无冲突队列;中性提示 + 空通道说明;conflicts 403 静默(§5.35) |
|
||||
| **仍关注** | 用法 | 通道断了靠智建自愈;表数据靠宇恒双向指纹 / 数据恢复;不是「再点启动」 |
|
||||
|
||||
### 0.0 修改流程(冻结)
|
||||
@@ -75,6 +77,7 @@
|
||||
| **Z32** | **绑定表单按方式显隐字段(2026-08-06 · 宇恒已改完)**:`visible_when` + sendFrom 过滤;绑码/手机号/稍后切换下方表单项。**智建无需改** |
|
||||
| **Z33** | **仅数据同步时勿因缺 app.read 进换绑(2026-08-06 · 宇恒已改完)**:GET /apps 403 且已 sync_bound → apps=[] 继续模块菜单并提示开通读取模块。**智建无需改**(控制台给智能体开「读取模块」即可列模块) |
|
||||
| **Z34** | **换机按账号恢复(2026-08-06 · 宇恒半程已改完)**:有手机号时可静默 ticket/confirm。**完整「仅宇恒 ID」见智建 Z34b(已落实)** |
|
||||
| **Z36** | **编辑发布无增量中文取消(2026-08-07 · 宇恒已改完;智建 Z36 已落实)**:宇恒侧预处理;智建 `host_meta_updated` / `[NO_BLUEPRINT_DELTA]`(§5.34) |
|
||||
|
||||
#### A′. 宇恒 · 配合注意(非阻塞新开发)
|
||||
|
||||
@@ -98,6 +101,8 @@
|
||||
| **数据恢复(§5.13)** | 成功同步后线上库快照(latest+previous);SyncPage「数据恢复」;`GET …/checkpoint` + `POST …/restore` |
|
||||
| **Z34b** | `POST /api/v1/auth/yuheng/restore-by-host`:HMAC 凭票(phone 可空)按 `host_key` 恢复已 sync_bound 落点并轮换密钥;见 §5.32 |
|
||||
| **Z35** | 绑定 `database_name=user_{id}`;publish 拒绝中文库名并纠正脏数据;见 §5.33 |
|
||||
| **Z36** | merge 无增量:有 `host_meta.module_name` → `publish_mode=host_meta_updated`;否则中文 `[NO_BLUEPRINT_DELTA]`;见 §5.34 |
|
||||
| **Z37** | SyncPage 无冲突队列;中性 LWW 说明 +「该公司暂无通道」空态;`listSyncConflicts` 403 静默;见 §5.35 |
|
||||
|
||||
> 产品一句:**账号已绑定 ⇒ 落点信息固定;通道没了平台自动补,终端无需手填 channel_id。**
|
||||
|
||||
@@ -105,7 +110,7 @@
|
||||
|
||||
| 优先级 | 编号 | 项 | 说明 |
|
||||
|--------|------|----|------|
|
||||
| — | — | (Z35 已合入见 B) | 其余无阻塞开发项;生产 pull 见 B′ |
|
||||
| — | — | (Z36/Z37 已合入见 B) | 当前无待开发项;运维见 B′ |
|
||||
|
||||
#### B‴. 智建 · 本次明确不改(Z15)
|
||||
|
||||
@@ -121,7 +126,7 @@
|
||||
|
||||
| 优先级 | 项 | 说明 |
|
||||
|--------|----|------|
|
||||
| **P0** | **生产 pull 本批** | Z10d + fingerprint + Z14c + **Z12h** + **数据恢复** + **Z34b** + **Z35**;`bash ./restart.sh --pull` |
|
||||
| **P0** | **生产 pull 本批** | Z10d + fingerprint + Z14c + **Z12h** + **数据恢复** + **Z34b** + **Z35** + **Z36** + **Z37**;`bash ./restart.sh --pull` |
|
||||
| **P0** | 成员手机 | 「宇信达」绑 **`13531041944`**;勿超管号 |
|
||||
| **P0** | 通道表白名单 | **勿**「填入测试默认」;形态 B 可空 |
|
||||
| **P1** | 凭票 Secret | `YuhengTicket.Secret` ≡ `YXD_YUHENG_TICKET_SECRET` |
|
||||
@@ -1123,6 +1128,76 @@ POST /api/v1/agent/sync/channels/{id}/pull
|
||||
2. 智能体 `database_name` 为合法 `a-z0-9_`;控制台展示名仍可为中文。
|
||||
3. 宇恒侧无需发版即可复测通过。
|
||||
|
||||
### 5.34 【Z36 · 2026-08-07】编辑发布 `nothing new to publish`
|
||||
|
||||
**状态**:**宇恒已改提示/预处理;智建已落实(须生产 pull)**。
|
||||
|
||||
#### 现象
|
||||
|
||||
编辑已有模块后发布:
|
||||
|
||||
`HTTP 400: nothing new to publish: provide newly generated pages ...`
|
||||
|
||||
请求多为 `mode=add_pages`,`host_meta.module_name` 可能已改(如「记录列表/概览」)。
|
||||
|
||||
#### 根因
|
||||
|
||||
`add_pages` 走蓝图 merge:生成草稿的 page id/route 与线上完全一致、且无新字段/资源时,merge 增量为空 → 400。仅改显示名(host_meta)也会踩中。
|
||||
|
||||
#### 宇恒改
|
||||
|
||||
1. 发布前 `prepare_add_pages_draft`:去掉完全重复页,冲突 id/route 改写。
|
||||
2. `draft_has_add_pages_delta` 判定无新页/新字段则中文取消发布,不再抛裸英文 400。
|
||||
3. 若仍收到该错误,中文说明(并指向本意见 Z36)。
|
||||
|
||||
#### 智建(Z36 · 已改)
|
||||
|
||||
`add_pages`/`auto` 在 merge 无增量时:若 `host_meta.module_name` 非空 → 只更新显示名并重签胶囊,返回 `publish_mode=host_meta_updated`;否则 400 中文 message 含 `[NO_BLUEPRINT_DELTA]`。有蓝图增量时亦把 `module_name` 写入蓝图 Meta.Name。
|
||||
|
||||
#### 验收
|
||||
|
||||
1. 编辑需求明确「新增某页」且生成含新页 → add_pages 成功。
|
||||
2. 生成仅复述旧页 / 仅改显示名 → 宇恒中文取消,不出现裸 `nothing new to publish`。
|
||||
3.(智建)仅改模块显示名再发布(带 `host_meta.module_name`)→ 2xx + `host_meta_updated`;无 module_name → 中文 `[NO_BLUEPRINT_DELTA]`。
|
||||
|
||||
### 5.35 【Z37 · 2026-08-07】超管进公司「数据同步」红条 LWW 403 + 空表
|
||||
|
||||
**状态**:**智建已落实(须生产 pull 前端 dist);宇恒勿改**。
|
||||
|
||||
#### 现象(生产控制台截图)
|
||||
|
||||
1. 平台超管在「租户/公司」点「管理该公司」(如「帅帅」)→ 绿条:`已打开「帅帅」管理视图(身份仍是平台部/超管)`(**预期**:身份仍是超管,只是切到该公司租户上下文)。
|
||||
2. 打开「数据同步」:顶部**红气泡** + 页内**红 Alert** 同文案:`冲突/LWW 覆盖日志仅平台超级管理员可查`(重复、吓人,像故障)。
|
||||
3. 「数据同步」通道表、`冲突队列(未解决)` 均为「暂无数据」。
|
||||
|
||||
#### 根因(产品设计 + 前端体验)
|
||||
|
||||
| 点 | 说明 |
|
||||
|----|------|
|
||||
| **权限设计(正确)** | 公司侧 `GET /api/v1/admin/sync/conflicts*` **固定 403**,文案即上句(`syncConflictsHandler`)。LWW 覆盖审计只在平台超管页:`GET /api/v1/platform/dbsync/lww-overrides`(`PlatformTenantsPage`「LWW 覆盖日志」)。方案/开通说明已写「公司侧无冲突台」。 |
|
||||
| **为何会弹 403** | 生产前端仍在公司 SyncPage 拉冲突队列(截图有「冲突队列」区块)。本仓当前 `SyncPage.tsx` **已去掉**冲突列表与 `listSyncConflicts` 调用,仅文案提示「覆盖审计仅超管可查」——**生产 web 未 pull/未发到与本仓一致**,或残留调用把 403 当成 `setError` + `message.error` 双报。 |
|
||||
| **空通道表** | 列表按**当前公司租户**过滤。进「帅帅」只看帅帅的通道;该公司从未绑智能体/无通道 →「暂无数据」正常。联调账号若在别的公司(如婷婷),须进**对应公司**管理视图,或回平台租户列表再进目标公司。 |
|
||||
| **绿条** | 管理视图提示,与红条无关;可保留。文案「平台部」vs 源码「平台超管」亦说明生产前端偏旧。 |
|
||||
|
||||
> 结论:**不是宇恒同步坏了,也不是超管没权限查 LWW**;是公司 SyncPage 仍把「公司侧禁用冲突 API」当错误展示,且可能看错公司导致通道为空。LWW 应去平台侧审计区查。
|
||||
|
||||
#### 智建改(须 · 已落实)
|
||||
|
||||
1. **生产 pull/发版前端**:SyncPage 与本仓一致——**无**「冲突队列」区块、**不**再调 `listSyncConflicts`(或 403 静默空列表)。
|
||||
2. 公司 SyncPage 顶栏中性说明(非 error):「冲突已自动 LWW;覆盖日志请到平台超管 · LWW 覆盖日志」。
|
||||
3. 空通道:空态「该公司暂无同步通道…」,勿与 LWW 403 混为一谈。
|
||||
4. 管理视图绿条文案统一为「身份仍是平台超管」。
|
||||
|
||||
#### 宇恒
|
||||
|
||||
**无需改代码、无需改契约。**
|
||||
|
||||
#### 验收
|
||||
|
||||
1. 超管 → 管理某公司 → 数据同步:**无**红条/红气泡「冲突/LWW…仅超管可查」;无冲突队列表。
|
||||
2. 该公司确有通道时列表可见;无通道时空态说明清楚,不报权限错误。
|
||||
3. 平台租户页「LWW 覆盖日志」仍可查审计;公司侧 conflicts 接口保持 403(契约不变)。
|
||||
|
||||
## 6. 联系与附件
|
||||
|
||||
- **待改清单(优先看)**:§0.2(**A′ 宇恒接入 restore-by-host · B′ 运维 pull**);**改代码前先写本意见**(§0.0);配合见 **§0.3**
|
||||
|
||||
Reference in New Issue
Block a user