35c79281c8
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.
88 lines
2.7 KiB
Go
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
|
|
}
|