Files

144 lines
4.1 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
// Package skillstate persists the official Skill snapshot and centralized
// ownership metadata written after setup and upgrade. Bundled skills are
// always fully refreshed from the current release and local absence is not an
// exclusion.
package skillstate
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/skillprovenance"
)
const stateFile = "skills-state.json"
// legacyOfficialSkillNames is the frozen set of official multi-skill names
// shipped before centralized ownership metadata. Exact names are safe
// migration evidence; a dingtalk-* prefix is not.
//
// Keep retired names here permanently so an old installation can still be
// migrated after that Skill has been folded into another bundle.
var legacyOfficialSkillNames = map[string]struct{}{
"dingtalk-agoal": {},
"dingtalk-aiapp": {},
"dingtalk-aisearch": {},
"dingtalk-aitable": {},
"dingtalk-attendance": {},
"dingtalk-calendar": {},
"dingtalk-chat": {},
"dingtalk-contact": {},
"dingtalk-dev": {},
"dingtalk-devapp": {},
"dingtalk-devdoc": {},
"dingtalk-ding": {},
"dingtalk-doc": {},
"dingtalk-drive": {},
"dingtalk-event": {},
"dingtalk-hrbrain": {},
"dingtalk-live": {},
"dingtalk-mail": {},
"dingtalk-markdown": {},
"dingtalk-minutes": {},
"dingtalk-misc": {},
"dingtalk-oa": {},
"dingtalk-pat": {},
"dingtalk-profile": {},
"dingtalk-report": {},
"dingtalk-shared": {},
"dingtalk-sheet": {},
"dingtalk-skill": {},
"dingtalk-todo": {},
"dingtalk-wiki": {},
"dws-shared": {},
}
var (
skillStateReadFile = os.ReadFile
skillStateRemove = os.Remove
)
type State struct {
Version string `json:"version"`
OfficialSkills []string `json:"official_skills"`
UpdatedSkills []string `json:"updated_skills"`
ManagedSkills []skillprovenance.Record `json:"managed_skills"`
UpdatedAt string `json:"updated_at"`
}
// IsLegacyOfficialSkillName reports whether name was an exact official
// multi-skill directory name before centralized ownership metadata shipped.
func IsLegacyOfficialSkillName(name string) bool {
_, ok := legacyOfficialSkillNames[name]
return ok
}
func Path(home string) string {
if configured := strings.TrimSpace(os.Getenv("DWS_CONFIG_DIR")); configured != "" {
return filepath.Join(configured, stateFile)
}
return filepath.Join(home, ".dws", stateFile)
}
func Read(home string) (*State, bool, error) {
data, err := skillStateReadFile(Path(home))
if err != nil {
if os.IsNotExist(err) {
return nil, false, nil
}
return nil, false, err
}
var state State
if err := json.Unmarshal(data, &state); err != nil {
return nil, false, fmt.Errorf("skill 状态不可读: %w", err)
}
return &state, true, nil
}
func Write(home string, state State) error {
state.OfficialSkills = uniqueSorted(state.OfficialSkills)
state.UpdatedSkills = uniqueSorted(state.UpdatedSkills)
state.ManagedSkills = skillprovenance.Merge(nil, state.ManagedSkills)
data, _ := json.MarshalIndent(state, "", " ")
if err := helpers.AtomicWriteJSON(Path(home), append(data, '\n')); err != nil {
return fmt.Errorf("保存 skill 状态失败: %w", err)
}
return nil
}
func ManagedSkillNames(state *State) map[string]bool {
if state == nil {
return map[string]bool{}
}
return skillprovenance.Names(state.ManagedSkills)
}
func Remove(home string) error {
if err := skillStateRemove(Path(home)); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("清理 skill 状态失败: %w", err)
}
return nil
}
func uniqueSorted(values []string) []string {
seen := make(map[string]bool, len(values))
out := make([]string, 0, len(values))
for _, value := range values {
if value == "" || seen[value] {
continue
}
seen[value] = true
out = append(out, value)
}
sort.Strings(out)
return out
}