From ae9caa06af63252692ea3602d3cebb50f12bec08 Mon Sep 17 00:00:00 2001 From: "liangxiaoqin.lxq" Date: Mon, 10 Aug 2026 11:08:18 +0800 Subject: [PATCH] =?UTF-8?q?cr=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/helpers/wiki.go | 73 ++---- internal/helpers/wiki_feed_format_test.go | 232 +++++++----------- skills/mono/references/products/wiki.md | 4 +- skills/multi/dingtalk-wiki/references/wiki.md | 4 +- 4 files changed, 111 insertions(+), 202 deletions(-) diff --git a/internal/helpers/wiki.go b/internal/helpers/wiki.go index 34296e29..78ef2f35 100644 --- a/internal/helpers/wiki.go +++ b/internal/helpers/wiki.go @@ -1325,7 +1325,7 @@ ORG 类型授权不会出现在查询结果中。`, 通过 --workspace 指定知识库,支持传入知识库 ID 或知识库 URL。 支持分页,通过 --cursor 传入上次返回的 nextToken 获取下一页。 -使用 --exclude-file 可排除文件相关动态(文档创建/文档更新等),仅保留文档操作(创建/更新/评论/点赞等)。 +使用 --exclude-file 可排除普通文件、媒体文件、文件夹及 Office 文件动态,仅保留在线文档操作(创建/更新/评论/点赞等)。 当用户要求"只看文档操作""排除文件上传"等意图时,必须使用此 flag, 禁止在客户端自行过滤。`, Example: ` dws wiki feed list --workspace @@ -1357,8 +1357,12 @@ ORG 类型授权不会出现在查询结果中。`, if err != nil { return err } - deps.Out.PrintJSON(formatFeedTime(text)) - return nil + // --format raw: 输出原始 MCP 文本,不做后处理 + if deps.Caller.Format() == "raw" { + deps.Out.PrintRaw(text) + return nil + } + return deps.Out.PrintJSON(formatFeedTime(text)) }, } DeclareLeafMetadata(feedListCmd, LeafSpec{ @@ -1406,7 +1410,7 @@ ORG 类型授权不会出现在查询结果中。`, feedListCmd.Flags().String("workspace", "", "知识库 ID 或 URL (必填)") feedListCmd.Flags().Int("limit", 0, "每页数量 (默认 10,最大 20)。用户未明确要求条数时禁止加此 flag,让服务端走默认 10") feedListCmd.Flags().String("cursor", "", "分页游标 (首页留空)") - feedListCmd.Flags().Bool("exclude-file", false, "排除文件相关动态(文档创建/更新/评论/点赞等),仅保留文档操作。用户要求排除文件/只看文档创建/更新/评论/点赞时必须使用此flag,禁止客户端过滤") + feedListCmd.Flags().Bool("exclude-file", false, "排除普通文件、媒体文件、文件夹及 Office 文件动态,仅保留在线文档操作(创建/更新/评论/点赞)。用户要求排除文件/只看文档操作时必须使用此 flag,禁止客户端过滤") feedListCmd.Flags().String("workspace-id", "", "") _ = feedListCmd.Flags().MarkHidden("workspace-id") RegisterCrossProductAliases(feedListCmd) @@ -1448,9 +1452,8 @@ ORG 类型授权不会出现在查询结果中。`, var beijingLoc = time.FixedZone("CST", 8*3600) // formatFeedTime 将知识库动态 feeds[].time 毫秒时间戳转为可读时间字符串, -// 同时保留原始毫秒值至 timeMs 字段。解析失败时原样返回,不阻断输出。 -// 同时裁剪冗余字段(avatarUrl、docKey、parentDoc 等),减小输出体积, -// 避免 Agent 因 JSON 过大而在后续处理中产生长耗时 echo 命令。 +// 写入 timeFormatted 字段,同时保留原始 time 毫秒时间戳不变(避免破坏已有脚本)。 +// 解析失败时原样返回,不阻断输出。 func formatFeedTime(raw string) any { var result map[string]any if err := json.Unmarshal([]byte(raw), &result); err != nil { @@ -1469,65 +1472,21 @@ func formatFeedTime(raw string) any { if !ok || ms <= 0 { continue } - item["timeMs"] = ms - item["time"] = time.UnixMilli(int64(ms)).In(beijingLoc).Format("2006-01-02 15:04") - trimFeedFields(item) + // 保留原始 time 毫秒时间戳,新增 timeFormatted 可读字段 + item["timeFormatted"] = time.UnixMilli(int64(ms)).In(beijingLoc).Format("2006-01-02 15:04") + enrichFeedFields(item) } return result } -// trimFeedFields 裁剪单条 feed 中对 Agent 无用的冗余字段, -// 大幅减小 JSON 输出体积,避免 Agent 二次处理时产生超长 echo 命令。 -func trimFeedFields(item map[string]any) { - // 移除大块冗余字段 - delete(item, "parentDoc") - delete(item, "userInfo") - +// enrichFeedFields 为单条 feed 补充可读字段(typeLabel), +func enrichFeedFields(item map[string]any) { // 将 type 数字映射为可读标签,使 Agent 无需查表即可直接展示 if typeNum, ok := item["type"].(float64); ok { if label, known := feedTypeLabels[int(typeNum)]; known { item["typeLabel"] = label } } - - // 裁剪 users:仅保留 nick - if users, ok := item["users"].([]any); ok { - for _, u := range users { - if user, ok := u.(map[string]any); ok { - nick := user["nick"] - for k := range user { - delete(user, k) - } - if nick != nil { - user["nick"] = nick - } - } - } - } - - // 裁剪 content:仅保留 content.doc.name 和 content.doc.extension - // extension 区分文档类型(adoc=文档, asheet=表格等),Agent 需要用来区分更新对象 - if content, ok := item["content"].(map[string]any); ok { - if doc, ok := content["doc"].(map[string]any); ok { - name := doc["name"] - extension := doc["extension"] - for k := range doc { - delete(doc, k) - } - if name != nil { - doc["name"] = name - } - if extension != nil { - doc["extension"] = extension - } - } - // 移除 content 中除 doc 外的其他字段(如 dentryKey / workspaceKey 等) - for k := range content { - if k != "doc" { - delete(content, k) - } - } - } } // feedTypeLabels 将知识库动态 type 数字映射为可读中文标签, @@ -1543,7 +1502,7 @@ var feedTypeLabels = map[int]string{ 7: "上传普通文件", 8: "上传媒体文件", 9: "上传文件夹", - 10: "上传文件夹", + 10: "上传文件夹 V2", 11: "加入团队", 12: "创建知识库", } diff --git a/internal/helpers/wiki_feed_format_test.go b/internal/helpers/wiki_feed_format_test.go index 35339b1e..1ea599f1 100644 --- a/internal/helpers/wiki_feed_format_test.go +++ b/internal/helpers/wiki_feed_format_test.go @@ -5,9 +5,7 @@ import ( "encoding/json" "io" "os" - "strings" "testing" - "time" ) // ---------- formatFeedTime unit tests ---------- @@ -104,16 +102,10 @@ func TestCrossPlatformCoverageFormatFeedTimeTimeMissingOrInvalid(t *testing.T) { } func TestCrossPlatformCoverageFormatFeedTimeNormalPath(t *testing.T) { - // 正常路径:毫秒时间戳被格式化,原始值保留到 timeMs - // 2025-06-15 10:30:00 UTC = 2025-06-15 18:30:00 CST (UTC+8) + // 正常路径:time 保留原始毫秒时间戳,timeFormatted 被格式化为北京时间 + // 1750067400000 ms = 2025-06-16 17:50 Beijing time (UTC+8) tsMs := float64(1750067400000) - raw := `{"feeds":[{"time":` + json.Number(strings.TrimRight(strings.TrimRight( - strings.Replace( - string(mustMarshal(t, map[string]any{"v": tsMs})), - `{"v":`, "", 1), - "}"), "")).String() + `,"type":1,"id":"f1"}]}` - // Build a cleaner JSON payload - raw = buildFeedJSON(t, []map[string]any{ + raw := buildFeedJSON(t, []map[string]any{ {"time": tsMs, "type": float64(1), "id": "f1"}, }) @@ -128,19 +120,18 @@ func TestCrossPlatformCoverageFormatFeedTimeNormalPath(t *testing.T) { } item := feeds[0].(map[string]any) - // timeMs 保留原始毫秒值 - if ms, ok := item["timeMs"].(float64); !ok || ms != tsMs { - t.Fatalf("timeMs = %v, want %v", item["timeMs"], tsMs) + // time 保留原始毫秒时间戳不变 + if ms, ok := item["time"].(float64); !ok || ms != tsMs { + t.Fatalf("time = %v (%T), want original ms %v", item["time"], item["time"], tsMs) } - // time 被格式化为北京时间字符串 - timeStr, ok := item["time"].(string) + // timeFormatted 被格式化为北京时间字符串(硬编码期望值) + timeStr, ok := item["timeFormatted"].(string) if !ok { - t.Fatalf("time = %T, want string", item["time"]) + t.Fatalf("timeFormatted = %T, want string", item["timeFormatted"]) } - expected := time.UnixMilli(int64(tsMs)).In(beijingLoc).Format("2006-01-02 15:04") - if timeStr != expected { - t.Fatalf("time = %q, want %q", timeStr, expected) + if timeStr != "2025-06-16 17:50" { + t.Fatalf("timeFormatted = %q, want %q", timeStr, "2025-06-16 17:50") } // typeLabel 被填充 @@ -166,124 +157,108 @@ func TestCrossPlatformCoverageFormatFeedTimeMultipleFeeds(t *testing.T) { // 前两个有时间戳 for i := 0; i < 2; i++ { item := feeds[i].(map[string]any) - if _, ok := item["timeMs"].(float64); !ok { - t.Fatalf("feed[%d] missing timeMs", i) + if _, ok := item["time"].(float64); !ok { + t.Fatalf("feed[%d] time should remain float64, got %T", i, item["time"]) } - if _, ok := item["time"].(string); !ok { - t.Fatalf("feed[%d] time not string", i) + if _, ok := item["timeFormatted"].(string); !ok { + t.Fatalf("feed[%d] timeFormatted not string", i) } } // 第三个没有时间戳 item3 := feeds[2].(map[string]any) - if _, hasTimeMs := item3["timeMs"]; hasTimeMs { - t.Fatalf("feed[2] should not have timeMs") + if _, hasTimeFormatted := item3["timeFormatted"]; hasTimeFormatted { + t.Fatalf("feed[2] should not have timeFormatted") } } -// ---------- trimFeedFields unit tests ---------- +// ---------- enrichFeedFields unit tests ---------- -func TestCrossPlatformCoverageTrimFeedFieldsRemovesParentDocAndUserInfo(t *testing.T) { +func TestCrossPlatformCoverageEnrichFeedFieldsPreservesAllFields(t *testing.T) { + // enrichFeedFields 不删除任何字段,仅新增 typeLabel item := map[string]any{ "parentDoc": map[string]any{"name": "parent"}, "userInfo": map[string]any{"nick": "user1"}, "id": "f1", + "type": float64(1), } - trimFeedFields(item) - if _, ok := item["parentDoc"]; ok { - t.Fatal("parentDoc should be removed") + enrichFeedFields(item) + if _, ok := item["parentDoc"]; !ok { + t.Fatal("parentDoc should be preserved") } - if _, ok := item["userInfo"]; ok { - t.Fatal("userInfo should be removed") + if _, ok := item["userInfo"]; !ok { + t.Fatal("userInfo should be preserved") } if item["id"] != "f1" { t.Fatal("id should be preserved") } + if item["typeLabel"] != "更新文档" { + t.Fatalf("typeLabel = %v, want 更新文档", item["typeLabel"]) + } } -func TestCrossPlatformCoverageTrimFeedFieldsTypeLabelKnown(t *testing.T) { +func TestCrossPlatformCoverageEnrichFeedFieldsTypeLabelKnown(t *testing.T) { // 已知 type → 添加 typeLabel for typeNum, expectedLabel := range feedTypeLabels { item := map[string]any{"type": float64(typeNum)} - trimFeedFields(item) + enrichFeedFields(item) if item["typeLabel"] != expectedLabel { t.Fatalf("type %d: typeLabel = %v, want %q", typeNum, item["typeLabel"], expectedLabel) } } } -func TestCrossPlatformCoverageTrimFeedFieldsTypeLabelUnknown(t *testing.T) { +func TestCrossPlatformCoverageEnrichFeedFieldsTypeLabelUnknown(t *testing.T) { // 未知 type → 不添加 typeLabel item := map[string]any{"type": float64(999)} - trimFeedFields(item) + enrichFeedFields(item) if _, ok := item["typeLabel"]; ok { t.Fatal("typeLabel should not be set for unknown type 999") } } -func TestCrossPlatformCoverageTrimFeedFieldsTypeNotFloat(t *testing.T) { +func TestCrossPlatformCoverageEnrichFeedFieldsTypeNotFloat(t *testing.T) { // type 不是 float64 → 不添加 typeLabel item := map[string]any{"type": "string-type"} - trimFeedFields(item) + enrichFeedFields(item) if _, ok := item["typeLabel"]; ok { t.Fatal("typeLabel should not be set for non-float type") } } -func TestCrossPlatformCoverageTrimFeedFieldsUsersTrim(t *testing.T) { - // users 数组中的 user 对象只保留 nick +func TestCrossPlatformCoverageEnrichFeedFieldsUsersPreserved(t *testing.T) { + // users 数组中的 user 对象所有字段保留不变 item := map[string]any{ "users": []any{ map[string]any{"nick": "Alice", "avatarUrl": "http://img", "userId": "u1"}, map[string]any{"nick": "Bob", "email": "bob@test"}, }, } - trimFeedFields(item) + enrichFeedFields(item) users := item["users"].([]any) - for i, u := range users { - user := u.(map[string]any) - if _, ok := user["nick"]; !ok { - t.Fatalf("users[%d] should have nick", i) - } - for k := range user { - if k != "nick" { - t.Fatalf("users[%d] unexpected field %q", i, k) - } - } + u0 := users[0].(map[string]any) + if u0["avatarUrl"] != "http://img" { + t.Fatalf("users[0].avatarUrl should be preserved, got %v", u0["avatarUrl"]) } - if users[0].(map[string]any)["nick"] != "Alice" { - t.Fatalf("users[0].nick = %v, want Alice", users[0].(map[string]any)["nick"]) + if u0["userId"] != "u1" { + t.Fatalf("users[0].userId should be preserved, got %v", u0["userId"]) } } -func TestCrossPlatformCoverageTrimFeedFieldsUsersNilNick(t *testing.T) { - // users 中 nick 为 nil → 删除后不回填 - item := map[string]any{ - "users": []any{ - map[string]any{"userId": "u1"}, - }, - } - trimFeedFields(item) - user := item["users"].([]any)[0].(map[string]any) - if len(user) != 0 { - t.Fatalf("user should be empty after trimming nil nick, got %v", user) - } -} - -func TestCrossPlatformCoverageTrimFeedFieldsUsersNonMapItem(t *testing.T) { +func TestCrossPlatformCoverageEnrichFeedFieldsUsersNonMapItem(t *testing.T) { // users 数组中有非 map 元素 → 不 panic item := map[string]any{ "users": []any{"not-a-map", 42, nil}, } - trimFeedFields(item) // 不应 panic + enrichFeedFields(item) // 不应 panic users := item["users"].([]any) if len(users) != 3 { t.Fatalf("users length changed: %d", len(users)) } } -func TestCrossPlatformCoverageTrimFeedFieldsContentDocTrim(t *testing.T) { - // content.doc 只保留 name 和 extension +func TestCrossPlatformCoverageEnrichFeedFieldsContentPreserved(t *testing.T) { + // content 和 content.doc 所有字段保留不变 item := map[string]any{ "content": map[string]any{ "doc": map[string]any{ @@ -296,78 +271,42 @@ func TestCrossPlatformCoverageTrimFeedFieldsContentDocTrim(t *testing.T) { "workspaceKey": "ws1", }, } - trimFeedFields(item) + enrichFeedFields(item) content := item["content"].(map[string]any) - // content 中只保留 doc - for k := range content { - if k != "doc" { - t.Fatalf("content should only have doc, found %q", k) - } + if content["dentryKey"] != "entry1" { + t.Fatalf("content.dentryKey = %v, want entry1", content["dentryKey"]) + } + if content["workspaceKey"] != "ws1" { + t.Fatalf("content.workspaceKey = %v, want ws1", content["workspaceKey"]) } doc := content["doc"].(map[string]any) if doc["name"] != "测试文档" { t.Fatalf("doc.name = %v, want 测试文档", doc["name"]) } - if doc["extension"] != "adoc" { - t.Fatalf("doc.extension = %v, want adoc", doc["extension"]) - } - for k := range doc { - if k != "name" && k != "extension" { - t.Fatalf("doc unexpected field %q", k) - } + if doc["docKey"] != "dk123" { + t.Fatalf("doc.docKey = %v, want dk123", doc["docKey"]) } } -func TestCrossPlatformCoverageTrimFeedFieldsContentDocNilFields(t *testing.T) { - // content.doc 中 name 和 extension 缺失 → 不回填 - item := map[string]any{ - "content": map[string]any{ - "doc": map[string]any{ - "docKey": "dk123", - }, - }, - } - trimFeedFields(item) - doc := item["content"].(map[string]any)["doc"].(map[string]any) - if len(doc) != 0 { - t.Fatalf("doc should be empty, got %v", doc) - } -} - -func TestCrossPlatformCoverageTrimFeedFieldsContentNoDoc(t *testing.T) { - // content 存在但没有 doc 子字段 → 移除其他字段 - item := map[string]any{ - "content": map[string]any{ - "dentryKey": "entry1", - "workspaceKey": "ws1", - }, - } - trimFeedFields(item) - content := item["content"].(map[string]any) - if len(content) != 0 { - t.Fatalf("content should be empty, got %v", content) - } -} - -func TestCrossPlatformCoverageTrimFeedFieldsNoContentNoUsers(t *testing.T) { - // 没有 content / users / type → 仅删除 parentDoc / userInfo +func TestCrossPlatformCoverageEnrichFeedFieldsNoContentNoUsers(t *testing.T) { + // 没有 content / users / type → 所有字段保留 item := map[string]any{ "id": "f1", "parentDoc": map[string]any{"x": 1}, } - trimFeedFields(item) - if _, ok := item["parentDoc"]; ok { - t.Fatal("parentDoc should be removed") + enrichFeedFields(item) + if _, ok := item["parentDoc"]; !ok { + t.Fatal("parentDoc should be preserved") } if item["id"] != "f1" { t.Fatal("id should be preserved") } } -func TestCrossPlatformCoverageTrimFeedFieldsEmptyItem(t *testing.T) { +func TestCrossPlatformCoverageEnrichFeedFieldsEmptyItem(t *testing.T) { // 空 map → 不 panic item := map[string]any{} - trimFeedFields(item) + enrichFeedFields(item) if len(item) != 0 { t.Fatalf("empty item changed: %v", item) } @@ -453,38 +392,49 @@ func TestCrossPlatformCoverageWikiFeedListFormatIntegration(t *testing.T) { t.Fatalf("feeds length unexpected: %v", parsed["feeds"]) } - // 第一个 feed 验证 timeMs 保留、time 格式化、字段裁剪 + // 第一个 feed 验证 time 保留、timeFormatted 格式化、所有原始字段保留 f0 := feeds[0].(map[string]any) - if f0["timeMs"] != ts { - t.Fatalf("timeMs = %v, want %v", f0["timeMs"], ts) + // time 保留原始毫秒时间戳 + if f0["time"] != ts { + t.Fatalf("time = %v, want %v", f0["time"], ts) } - if _, ok := f0["time"].(string); !ok { - t.Fatalf("time should be string, got %T", f0["time"]) + // timeFormatted 被填充 + if _, ok := f0["timeFormatted"].(string); !ok { + t.Fatalf("timeFormatted should be string, got %T", f0["timeFormatted"]) } - if _, ok := f0["parentDoc"]; ok { - t.Fatal("parentDoc should be trimmed") + // 所有原始字段保留(不删除任何字段) + if _, ok := f0["parentDoc"]; !ok { + t.Fatal("parentDoc should be preserved") } - if _, ok := f0["userInfo"]; ok { - t.Fatal("userInfo should be trimmed") + if _, ok := f0["userInfo"]; !ok { + t.Fatal("userInfo should be preserved") } if f0["typeLabel"] != "创建文档" { t.Fatalf("typeLabel = %v, want 创建文档", f0["typeLabel"]) } - // users 裁剪验证 + // users 保留验证:avatarUrl / userId 均保留 if users, ok := f0["users"].([]any); ok { - for _, u := range users { - um := u.(map[string]any) - if _, ok := um["avatarUrl"]; ok { - t.Fatal("user avatarUrl should be trimmed") - } + u0 := users[0].(map[string]any) + if u0["avatarUrl"] != "http://avatar" { + t.Fatalf("users[0].avatarUrl should be preserved, got %v", u0["avatarUrl"]) + } + if u0["userId"] != "u1" { + t.Fatalf("users[0].userId should be preserved, got %v", u0["userId"]) } } - // content 裁剪验证 + // content 结构保留验证 if content, ok := f0["content"].(map[string]any); ok { - if _, ok := content["dentryKey"]; ok { - t.Fatal("content.dentryKey should be trimmed") + if content["dentryKey"] != "entry1" { + t.Fatalf("content.dentryKey should be preserved, got %v", content["dentryKey"]) + } + if content["workspaceKey"] != "ws1" { + t.Fatalf("content.workspaceKey should be preserved, got %v", content["workspaceKey"]) + } + doc := content["doc"].(map[string]any) + if doc["docKey"] != "dk123" { + t.Fatalf("content.doc.docKey should be preserved, got %v", doc["docKey"]) } } } diff --git a/skills/mono/references/products/wiki.md b/skills/mono/references/products/wiki.md index 9cc2b2d9..c06da8f6 100644 --- a/skills/mono/references/products/wiki.md +++ b/skills/mono/references/products/wiki.md @@ -196,7 +196,7 @@ Flags: --workspace string 知识库 ID 或 URL (必填) --limit int 每页数量 (默认 10,最大 20)。用户未明确要求条数时禁止加此 flag,让服务端走默认 10 --cursor string 分页游标 (首页留空) - --exclude-file 排除文件相关动态(上传/更新 office 等),仅保留文档操作 (默认 false) + --exclude-file 排除普通文件、媒体文件、文件夹及 Office 文件动态,仅保留在线文档操作 (默认 false) ``` 查询指定知识库的动态,返回谁在什么时间进行了更新、上传、评论等操作。 @@ -225,7 +225,7 @@ Flags: > **重要 — 展示规则(必须严格遵守)**: > 1. **typeLabel 字段**:CLI 已自动将 `type` 数字映射为 `typeLabel` 中文标签(如 `更新文档`、`创建文档`),**直接使用 `typeLabel` 展示即可**,无需自行查表映射。 -> 2. **time 字段**:已是北京时间字符串(格式如 `2026-07-30 08:00`),直接展示即可,无需任何转换。原始毫秒时间戳保存在 `timeMs` 字段中备用。 +> 2. **timeFormatted 字段**:已是北京时间字符串(格式如 `2026-07-30 08:00`),直接展示即可,无需任何转换。原始毫秒时间戳保存在 `time` 字段中(数字类型)。 ### 列出知识库节点 ``` diff --git a/skills/multi/dingtalk-wiki/references/wiki.md b/skills/multi/dingtalk-wiki/references/wiki.md index 58fb7f4b..c02e6fec 100644 --- a/skills/multi/dingtalk-wiki/references/wiki.md +++ b/skills/multi/dingtalk-wiki/references/wiki.md @@ -186,7 +186,7 @@ Flags: --workspace string 知识库 ID 或 URL (必填) --limit int 每页数量 (默认 10,最大 20)。用户未明确要求条数时禁止加此 flag,让服务端走默认 10 --cursor string 分页游标 (首页留空) - --exclude-file 排除文件相关动态(上传/更新 office 等),仅保留文档操作 (默认 false) + --exclude-file 排除普通文件、媒体文件、文件夹及 Office 文件动态,仅保留在线文档操作 (默认 false) ``` 查询指定知识库的动态,返回谁在什么时间进行了更新、上传、评论等操作。 @@ -215,7 +215,7 @@ Flags: > **重要 — 展示规则(必须严格遵守)**: > 1. **typeLabel 字段**:CLI 已自动将 `type` 数字映射为 `typeLabel` 中文标签(如 `更新文档`、`创建文档`),**直接使用 `typeLabel` 展示即可**,无需自行查表映射。 -> 2. **time 字段**:已是北京时间字符串(格式如 `2026-07-30 08:00`),直接展示即可,无需任何转换。原始毫秒时间戳保存在 `timeMs` 字段中备用。 +> 2. **timeFormatted 字段**:已是北京时间字符串(格式如 `2026-07-30 08:00`),直接展示即可,无需任何转换。原始毫秒时间戳保存在 `time` 字段中(数字类型)。 ### 列出知识库节点 ```