144 lines
4.1 KiB
Go
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
|
|
}
|