Files
Enterprise-Knowledge-Base/cli/cmd/doc/view.go
T
nullkey 35c79281c8 feat(cli): doc view + unlink (fill v0.3 design-gap audit)
Final design-pass audit on the v0.3 surface flagged two real gaps.

(A) doc view <id> was missing. Every other resource subtree exposes
a view verb (kb view, session view) for inspecting a single record,
but doc — which has the richest metadata of the three (title, file
name, type, size, parse_status, embedding_model, processed_at,
error_message) — had no single-doc surface. Users wanting one
doc's metadata had to `doc list | grep`.

Implementation mirrors kb view: narrow ViewService(GetKnowledge)
interface, --json envelope path, human KEY: VALUE renderer. Optional
fields are omitted rather than rendered as "-" so the panel is
dense. Tested: human renderer, title fallback when FileName empty,
omit-empty contract, JSON envelope shape, 404 classification.

(B) link had no counterpart. Once .weknora/project.yaml is written,
the only way to clear it was `rm` by hand. Both vercel and netlify
ship `unlink` as a top-level verb; not having one was a
discoverability gap. Top-level rather than `link --clear` follows
the verb-noun convention of the rest of the surface — the verb
stands alone and the operation isn't parameterised.

unlink walks up from cwd via projectlink.Discover (the same
parent-chain logic Factory.ResolveKB uses on the read side), so a
user in a subdirectory of a linked project can unlink without
cd-ing up. Errors with input.invalid_argument when no link is
found anywhere in the chain. Idempotent under racy concurrent
removal: os.ErrNotExist on os.Remove falls through to a Success
envelope since the post-condition holds either way.

projectlink package gained Remove() alongside Save / Load /
Discover so unlink doesn't reimplement the idempotent-remove
pattern inline.

Top-level registration in cmd/root.go, alongside link.
cli/AGENTS.md verb canon line adds unlink to the locally-introduced
list. cli/CHANGELOG.md gains an Added entry for each.

5 unit tests for view + 4 for unlink (cwd / walk-up / no-link
error / JSON envelope). Full suite green.

Intentionally deferred:
- session edit (rename a session): sessions auto-name from the
  first prompt; polish rather than a gap.
- link --clear as an alternative to unlink: top-level unlink is
  the documented form; aliases would just multiply the surface.
2026-05-14 10:57:17 +08:00

88 lines
2.7 KiB
Go

package doc
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/Tencent/WeKnora/cli/internal/agent"
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
"github.com/Tencent/WeKnora/cli/internal/format"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
"github.com/Tencent/WeKnora/cli/internal/text"
sdk "github.com/Tencent/WeKnora/client"
)
type ViewOptions struct {
JSONOut bool
}
// ViewService is the narrow SDK surface this command depends on.
type ViewService interface {
GetKnowledge(ctx context.Context, id string) (*sdk.Knowledge, error)
}
// NewCmdView builds `weknora doc view <id>`.
func NewCmdView(f *cmdutil.Factory) *cobra.Command {
opts := &ViewOptions{}
cmd := &cobra.Command{
Use: "view <id>",
Short: "Show a document by ID",
Example: ` weknora doc view doc_abc
weknora doc view doc_abc --json`,
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
cli, err := f.Client()
if err != nil {
return err
}
return runView(c.Context(), opts, cli, args[0])
},
}
cmd.Flags().BoolVar(&opts.JSONOut, "json", false, "Output JSON envelope")
agent.SetAgentHelp(cmd, "Returns the full Knowledge object for one document: id, kb_id, file metadata, parse_status, processed_at, error_message. Use this instead of paging through `doc list` when you have the doc id.")
return cmd
}
func runView(ctx context.Context, opts *ViewOptions, svc ViewService, id string) error {
doc, err := svc.GetKnowledge(ctx, id)
if err != nil {
return cmdutil.WrapHTTP(err, "get document %q", id)
}
if opts.JSONOut {
return format.WriteEnvelope(iostreams.IO.Out, format.Success(doc, &format.Meta{KBID: doc.KnowledgeBaseID}))
}
w := iostreams.IO.Out
fmt.Fprintf(w, "ID: %s\n", doc.ID)
fmt.Fprintf(w, "NAME: %s\n", text.KnowledgeDisplayName(doc.FileName, doc.Title, doc.ID))
if doc.KnowledgeBaseID != "" {
fmt.Fprintf(w, "KB: %s\n", doc.KnowledgeBaseID)
}
if doc.FileType != "" {
fmt.Fprintf(w, "TYPE: %s\n", doc.FileType)
}
if doc.FileSize > 0 {
fmt.Fprintf(w, "SIZE: %s\n", formatSize(doc.FileSize))
}
if doc.ParseStatus != "" {
fmt.Fprintf(w, "STATUS: %s\n", doc.ParseStatus)
}
if doc.EmbeddingModelID != "" {
fmt.Fprintf(w, "EMBEDDING: %s\n", doc.EmbeddingModelID)
}
if !doc.CreatedAt.IsZero() {
fmt.Fprintf(w, "CREATED: %s\n", doc.CreatedAt.Format("2006-01-02 15:04:05"))
}
if !doc.UpdatedAt.IsZero() {
fmt.Fprintf(w, "UPDATED: %s\n", doc.UpdatedAt.Format("2006-01-02 15:04:05"))
}
if doc.ProcessedAt != nil && !doc.ProcessedAt.IsZero() {
fmt.Fprintf(w, "PROCESSED: %s\n", doc.ProcessedAt.Format("2006-01-02 15:04:05"))
}
if doc.ErrorMessage != "" {
fmt.Fprintf(w, "ERROR: %s\n", doc.ErrorMessage)
}
return nil
}