Skip to content

Commit 62cbff5

Browse files
committed
fix: add atomic overwrite for whiteboard +update
Change-Id: I995e9575e333d4c2925eac75a52e4442dc570c5c
1 parent fe41234 commit 62cbff5

3 files changed

Lines changed: 28 additions & 257 deletions

File tree

shortcuts/whiteboard/shortcuts.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ type WbCliOutput struct {
2323
}
2424

2525
type WbCliOutputData struct {
26-
To string `json:"to"`
27-
Result interface{} `json:"result"`
26+
To string `json:"to"`
27+
Result struct {
28+
Nodes []interface{} `json:"nodes"`
29+
} `json:"result"`
2830
}

shortcuts/whiteboard/whiteboard_update.go

Lines changed: 22 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"net/http"
1212
"net/url"
1313
"strings"
14-
"time"
1514

1615
"github.com/larksuite/cli/internal/output"
1716
"github.com/larksuite/cli/internal/validate"
@@ -31,9 +30,8 @@ var formatCodeMap = map[string]int{
3130
FormatMermaid: 2,
3231
}
3332

34-
var wbUpdateScopes = []string{"board:whiteboard:node:read", "board:whiteboard:node:create", "board:whiteboard:node:delete"}
33+
var wbUpdateScopes = []string{"board:whiteboard:node:create", "board:whiteboard:node:delete"}
3534
var wbUpdateAuthTypes = []string{"user", "bot"}
36-
var skipDeleteNodesBatchSleep = false // for accelerate UT testing only
3735
var wbUpdateFlags = []common.Flag{
3836
{Name: "idempotent-token", Desc: "idempotent token to ensure the update is idempotent. Default is empty. min length is 10.", Required: false},
3937
{Name: "whiteboard-token", Desc: "whiteboard token of the whiteboard to update. You will need edit permission to update the whiteboard.", Required: true},
@@ -82,19 +80,6 @@ func wbUpdateDryRun(ctx context.Context, runtime *common.RuntimeContext) *common
8280
token := runtime.Str("whiteboard-token")
8381
overwrite := runtime.Bool("overwrite")
8482
descStr := "will call whiteboard open api to update content."
85-
var delNum int
86-
var err error
87-
if overwrite {
88-
// 还是会读取一下 whiteboard nodes,确认是否有节点要删除
89-
delNum, _, err = clearWhiteboardContent(ctx, runtime, token, []string{}, true)
90-
if err != nil {
91-
return common.NewDryRunAPI().Desc("read whiteboard nodes failed: " + err.Error())
92-
}
93-
if delNum > 0 {
94-
descStr += fmt.Sprintf(" %d existing nodes deleted before update.", delNum)
95-
}
96-
}
97-
9883
desc := common.NewDryRunAPI().Desc(descStr)
9984

10085
switch format {
@@ -103,24 +88,23 @@ func wbUpdateDryRun(ctx context.Context, runtime *common.RuntimeContext) *common
10388
if err != nil {
10489
return common.NewDryRunAPI().Desc("parse input failed: " + err.Error())
10590
}
106-
desc.POST(fmt.Sprintf("/open-apis/board/v1/whiteboards/%s/nodes", common.MaskToken(url.PathEscape(token)))).Body(nodes).Desc("create all nodes of the whiteboard.")
91+
reqBody := rawNodesCreateReq{
92+
Nodes: nodes,
93+
Overwrite: overwrite,
94+
}
95+
desc.POST(fmt.Sprintf("/open-apis/board/v1/whiteboards/%s/nodes", common.MaskToken(url.PathEscape(token)))).Body(reqBody).Desc("create all nodes of the whiteboard.")
10796
case FormatPlantUML, FormatMermaid:
10897
syntaxType := formatCodeMap[format]
10998
reqBody := plantumlCreateReq{
11099
PlantUmlCode: input,
111100
SyntaxType: syntaxType,
112101
ParseMode: 1,
113102
DiagramType: 0,
103+
Overwrite: overwrite,
114104
}
115105
desc.POST(fmt.Sprintf("/open-apis/board/v1/whiteboards/%s/nodes/plantuml", common.MaskToken(url.PathEscape(token)))).Body(reqBody).Desc(fmt.Sprintf("create %s node on the whiteboard.", format))
116106
}
117107

118-
if overwrite && delNum > 0 {
119-
// 在 DryRun 中只记录意图,不实际拉取和计算节点
120-
desc.GET(fmt.Sprintf("/open-apis/board/v1/whiteboards/%s/nodes", common.MaskToken(url.PathEscape(token)))).Desc("get all nodes of the whiteboard to delete, then filter out newly created ones.")
121-
desc.DELETE(fmt.Sprintf("/open-apis/board/v1/whiteboards/%s/nodes/batch_delete", common.MaskToken(url.PathEscape(token)))).Body("{\"ids\":[\"...\"]}").
122-
Desc(fmt.Sprintf("delete all old nodes of the whiteboard 100 nodes at a time. This API may be called multiple times and is not reversible. %d whiteboard nodes will be deleted while update.", delNum))
123-
}
124108
return desc
125109
}
126110

@@ -185,31 +169,17 @@ type createResponse struct {
185169
} `json:"data"`
186170
}
187171

188-
type deleteResponse struct {
189-
Code int `json:"code"`
190-
Msg string `json:"msg"`
191-
}
192-
193-
type simpleNodeResp struct {
194-
Code int `json:"code"`
195-
Msg string `json:"msg"`
196-
Data struct {
197-
Nodes []struct {
198-
Id string `json:"id"`
199-
Children []string `json:"children"`
200-
} `json:"nodes"`
201-
} `json:"data"`
202-
}
203-
204-
type deleteNodeReqBody struct {
205-
Ids []string `json:"ids"`
206-
}
207-
208172
type plantumlCreateReq struct {
209173
PlantUmlCode string `json:"plant_uml_code"`
210174
SyntaxType int `json:"syntax_type"`
211175
DiagramType int `json:"diagram_type,omitempty"`
212176
ParseMode int `json:"parse_mode,omitempty"`
177+
Overwrite bool `json:"overwrite,omitempty"`
178+
}
179+
180+
type rawNodesCreateReq struct {
181+
Nodes []interface{} `json:"nodes"`
182+
Overwrite bool `json:"overwrite,omitempty"`
213183
}
214184

215185
type plantumlCreateResp struct {
@@ -220,7 +190,7 @@ type plantumlCreateResp struct {
220190
} `json:"data"`
221191
}
222192

223-
func parseWBcliNodes(rawjson []byte) (wbNodes interface{}, err error, isRaw bool) {
193+
func parseWBcliNodes(rawjson []byte) (wbNodes []interface{}, err error, isRaw bool) {
224194
var wbOutput WbCliOutput
225195
if err := json.Unmarshal(rawjson, &wbOutput); err != nil {
226196
return nil, output.Errorf(output.ExitValidation, "parsing", fmt.Sprintf("unmarshal input json failed: %v", err)), false
@@ -229,121 +199,14 @@ func parseWBcliNodes(rawjson []byte) (wbNodes interface{}, err error, isRaw bool
229199
return nil, output.Errorf(output.ExitValidation, "whiteboard-cli", "whiteboard-cli failed. please check previous log."), false
230200
}
231201
if wbOutput.RawNodes != nil {
232-
wbNodes = struct {
233-
Nodes []interface{} `json:"nodes"`
234-
}{
235-
Nodes: wbOutput.RawNodes,
236-
}
202+
wbNodes = wbOutput.RawNodes
237203
isRaw = true
238204
} else {
239-
wbNodes = wbOutput.Data.Result
205+
wbNodes = wbOutput.Data.Result.Nodes
240206
}
241207
return wbNodes, nil, isRaw
242208
}
243209

244-
func clearWhiteboardContent(ctx context.Context, runtime *common.RuntimeContext, wbToken string, newNodeIDs []string, dryRun bool) (int, []string, error) {
245-
resp, err := runtime.DoAPI(&larkcore.ApiReq{
246-
HttpMethod: http.MethodGet,
247-
ApiPath: fmt.Sprintf("/open-apis/board/v1/whiteboards/%s/nodes", url.PathEscape(wbToken)),
248-
})
249-
if err != nil {
250-
return 0, nil, output.ErrNetwork(fmt.Sprintf("get whiteboard nodes failed: %v", err))
251-
}
252-
if resp.StatusCode != http.StatusOK {
253-
return 0, nil, output.ErrAPI(resp.StatusCode, string(resp.RawBody), nil)
254-
}
255-
var nodes simpleNodeResp
256-
err = json.Unmarshal(resp.RawBody, &nodes)
257-
if err != nil {
258-
return 0, nil, output.Errorf(output.ExitInternal, "parsing", fmt.Sprintf("parse whiteboard nodes failed: %v", err))
259-
}
260-
if nodes.Code != 0 {
261-
return 0, nil, output.ErrAPI(nodes.Code, "get whiteboard nodes failed", fmt.Sprintf("get whiteboard nodes failed: %s", nodes.Msg))
262-
}
263-
264-
// 收集所有新节点及其 children 的 ID,递归处理
265-
protectedIDs := make(map[string]bool)
266-
for _, id := range newNodeIDs {
267-
protectedIDs[id] = true
268-
}
269-
// 构建 node map 以便快速查找
270-
nodeMap := make(map[string][]string)
271-
if nodes.Data.Nodes != nil {
272-
for _, node := range nodes.Data.Nodes {
273-
nodeMap[node.Id] = node.Children
274-
}
275-
}
276-
// 递归收集所有 children
277-
visited := make(map[string]bool)
278-
var collectChildren func(id string)
279-
collectChildren = func(id string) {
280-
if visited[id] {
281-
return
282-
}
283-
visited[id] = true
284-
if children, ok := nodeMap[id]; ok {
285-
for _, child := range children {
286-
protectedIDs[child] = true
287-
collectChildren(child)
288-
}
289-
}
290-
}
291-
for _, id := range newNodeIDs {
292-
collectChildren(id)
293-
}
294-
295-
// 确定要删除的节点
296-
nodeIds := make([]string, 0, len(nodes.Data.Nodes))
297-
if nodes.Data.Nodes != nil {
298-
for _, node := range nodes.Data.Nodes {
299-
nodeIds = append(nodeIds, node.Id)
300-
}
301-
}
302-
delIds := make([]string, 0, len(nodeIds))
303-
for _, nodeId := range nodeIds {
304-
if !protectedIDs[nodeId] {
305-
delIds = append(delIds, nodeId)
306-
}
307-
}
308-
if dryRun {
309-
return len(delIds), delIds, nil
310-
}
311-
// 实际删除节点,按每批最多100个进行切分
312-
for i := 0; i < len(delIds); i += 100 {
313-
if !skipDeleteNodesBatchSleep {
314-
time.Sleep(time.Millisecond * 1000) // 画板内删除大量节点时,内部会有大量写操作,需要稍等一下,避免被限流
315-
}
316-
end := i + 100
317-
if end > len(delIds) {
318-
end = len(delIds)
319-
}
320-
batchIds := delIds[i:end]
321-
delReq := deleteNodeReqBody{
322-
Ids: batchIds,
323-
}
324-
resp, err = runtime.DoAPI(&larkcore.ApiReq{
325-
HttpMethod: http.MethodDelete,
326-
ApiPath: fmt.Sprintf("/open-apis/board/v1/whiteboards/%s/nodes/batch_delete", url.PathEscape(wbToken)),
327-
Body: delReq,
328-
})
329-
if err != nil {
330-
return 0, nil, output.ErrNetwork(fmt.Sprintf("delete whiteboard nodes failed: %v", err))
331-
}
332-
if resp.StatusCode != http.StatusOK {
333-
return 0, nil, output.ErrAPI(resp.StatusCode, string(resp.RawBody), nil)
334-
}
335-
var delResp deleteResponse
336-
err = json.Unmarshal(resp.RawBody, &delResp)
337-
if err != nil {
338-
return 0, nil, output.Errorf(output.ExitInternal, "parsing", fmt.Sprintf("parse whiteboard delete response failed: %v", err))
339-
}
340-
if delResp.Code != 0 {
341-
return 0, nil, output.ErrAPI(delResp.Code, "delete whiteboard nodes failed", fmt.Sprintf("delete whiteboard nodes failed: %s", delResp.Msg))
342-
}
343-
}
344-
return len(delIds), delIds, nil
345-
}
346-
347210
// updateWhiteboardByCode 使用 plantuml/mermaid 代码更新画板
348211
func updateWhiteboardByCode(ctx context.Context, runtime *common.RuntimeContext, wbToken string, input []byte, format string, overwrite bool, idempotentToken string) error {
349212
syntaxType := formatCodeMap[format]
@@ -352,6 +215,7 @@ func updateWhiteboardByCode(ctx context.Context, runtime *common.RuntimeContext,
352215
SyntaxType: syntaxType,
353216
ParseMode: 1,
354217
DiagramType: 0, // 0 表示自动识别
218+
Overwrite: overwrite,
355219
}
356220

357221
req := &larkcore.ApiReq{
@@ -383,20 +247,7 @@ func updateWhiteboardByCode(ctx context.Context, runtime *common.RuntimeContext,
383247

384248
outData := make(map[string]string)
385249
outData["created_node_id"] = createResp.Data.NodeID
386-
newNodeIDs := []string{createResp.Data.NodeID}
387-
388-
if overwrite {
389-
numNodes, _, err := clearWhiteboardContent(ctx, runtime, wbToken, newNodeIDs, false)
390-
if err != nil {
391-
return err
392-
}
393-
outData["deleted_nodes_num"] = fmt.Sprintf("%d", numNodes)
394-
}
395-
396250
runtime.OutFormat(outData, nil, func(w io.Writer) {
397-
if outData["deleted_nodes_num"] != "" {
398-
fmt.Fprintf(w, "%s existing nodes deleted.\n", outData["deleted_nodes_num"])
399-
}
400251
if outData["created_node_id"] != "" {
401252
fmt.Fprintf(w, "New node created.\n")
402253
}
@@ -413,11 +264,15 @@ func updateWhiteboardByRawNodes(ctx context.Context, runtime *common.RuntimeCont
413264
return err
414265
}
415266
outData := make(map[string]string)
267+
reqBody := rawNodesCreateReq{
268+
Nodes: nodes,
269+
Overwrite: overwrite,
270+
}
416271

417272
req := &larkcore.ApiReq{
418273
HttpMethod: http.MethodPost,
419274
ApiPath: fmt.Sprintf("/open-apis/board/v1/whiteboards/%s/nodes", url.PathEscape(wbToken)),
420-
Body: nodes,
275+
Body: reqBody,
421276
QueryParams: map[string][]string{},
422277
}
423278
if idempotentToken != "" {
@@ -452,19 +307,7 @@ func updateWhiteboardByRawNodes(ctx context.Context, runtime *common.RuntimeCont
452307
}
453308

454309
outData["created_node_ids"] = strings.Join(createResp.Data.NodeIDs, ",")
455-
456-
if overwrite {
457-
numNodes, _, err := clearWhiteboardContent(ctx, runtime, wbToken, createResp.Data.NodeIDs, false)
458-
if err != nil {
459-
return err
460-
}
461-
outData["deleted_nodes_num"] = fmt.Sprintf("%d", numNodes)
462-
}
463-
464310
runtime.OutFormat(outData, nil, func(w io.Writer) {
465-
if outData["deleted_nodes_num"] != "" {
466-
fmt.Fprintf(w, "%s existing nodes deleted.\n", outData["deleted_nodes_num"])
467-
}
468311
if outData["created_node_ids"] != "" {
469312
fmt.Fprintf(w, "%d new nodes created.\n", len(createResp.Data.NodeIDs))
470313
}

0 commit comments

Comments
 (0)