Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0de6e46ef4 | ||
|
|
9a8157a4cd | ||
|
|
b8e915edfb | ||
|
|
bd370ed0e9 |
@@ -1,8 +1,9 @@
|
||||
# 此 workflow 把本仓库代码自动镜像到 Gitee,供国内用户访问。
|
||||
# 需要在仓库 Settings → Secrets and variables → Actions 配置两个 secret:
|
||||
# GITEE_TOKEN —— Gitee 私人令牌(在 Gitee 设置→私人令牌生成),需勾选 projects + admin:repo_hook 权限。
|
||||
# GITEE_PRIVATE_KEY —— SSH 私钥(本地 ssh-keygen 生成的私钥),对应公钥需添加到 Gitee 账号的 SSH 公钥中。
|
||||
# 未配置 secret 时(例如 fork 仓库),mirror step 会被自动跳过,不会报错(不会出现红叉)。
|
||||
# 把本仓库代码自动镜像到 Gitee,供国内用户访问(raw 脚本入口 + tags)。
|
||||
# 用 HTTPS + 令牌直接 git push(无需 SSH key),复用已配置的 secret:
|
||||
# GITEE_TOKEN —— Gitee 私人令牌(勾 projects)
|
||||
# GITEE_USER —— 令牌所属 Gitee 用户名(用于 https 推送鉴权)
|
||||
# GITEE_REPO —— "owner/repo",如 DingTalk-Real-AI/dingtalk-workspace-cli
|
||||
# 未配置 GITEE_TOKEN 时(如 fork)自动跳过,不报红叉。
|
||||
name: Mirror code to Gitee
|
||||
|
||||
on:
|
||||
@@ -18,22 +19,26 @@ on:
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
# GitHub Actions 不允许在 job-level if 直接引用 secrets,因此先用 env 暴露 secret,
|
||||
# 再在 step 里做守卫判断。hub-mirror 必须有 SSH 私钥(GITEE_PRIVATE_KEY)才能推送,
|
||||
# 故以它为守卫:未配置时整步跳过(代码镜像可改由 Gitee 侧 pull-mirror 承担),不报红叉。
|
||||
# GitHub Actions 不允许在 job-level if 直接引用 secrets,故先用 env 暴露再在 step 守卫。
|
||||
env:
|
||||
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
|
||||
GITEE_PRIVATE_KEY: ${{ secrets.GITEE_PRIVATE_KEY }}
|
||||
GITEE_USER: ${{ secrets.GITEE_USER }}
|
||||
GITEE_REPO: ${{ secrets.GITEE_REPO }}
|
||||
steps:
|
||||
- name: Mirror to Gitee
|
||||
if: env.GITEE_PRIVATE_KEY != ''
|
||||
uses: Yikun/hub-mirror-action@master
|
||||
- name: Checkout (full history + tags)
|
||||
if: env.GITEE_TOKEN != ''
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
src: "github/DingTalk-Real-AI"
|
||||
dst: "gitee/DingTalk-Real-AI"
|
||||
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
|
||||
dst_token: ${{ secrets.GITEE_TOKEN }}
|
||||
account_type: "org"
|
||||
static_list: "dingtalk-workspace-cli"
|
||||
clone_style: "https"
|
||||
force_update: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Push main + tags to Gitee
|
||||
if: env.GITEE_TOKEN != ''
|
||||
run: |
|
||||
set -eu
|
||||
REMOTE="https://${GITEE_USER}:${GITEE_TOKEN}@gitee.com/${GITEE_REPO}.git"
|
||||
# 取到 main 与所有 tag(落到 origin/* 与本地 tags,避免推当前分支引用冲突)
|
||||
git fetch --force --tags origin 'refs/heads/main:refs/remotes/origin/main'
|
||||
# 镜像对齐(force:Gitee 始终跟随 GitHub)
|
||||
git push --force "$REMOTE" 'refs/remotes/origin/main:refs/heads/main'
|
||||
git push --force --tags "$REMOTE"
|
||||
echo "✅ 已镜像 main + tags 到 Gitee ${GITEE_REPO}"
|
||||
|
||||
@@ -6,6 +6,18 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.41] - 2026-06-24
|
||||
|
||||
This release makes the installers work from mainland China out of the box (no env var) and keeps the Gitee mirror in sync automatically.
|
||||
|
||||
### Added
|
||||
|
||||
- **Auto-fallback to the Gitee mirror when GitHub is unreachable** (#492; `scripts/install.sh`, `scripts/install.ps1`, `scripts/install-skills.sh`) — the installers probe GitHub Releases on startup and, when it is unreachable (typical in mainland China), automatically resolve the version and download every asset (binary, `checksums.txt`, `dws-skills.zip`) from the Gitee mirror instead. A plain `curl … | sh` now works in China with no `DWS_GITEE_REPO` needed. Explicit `DWS_GITEE_REPO` still wins, `DWS_NO_FALLBACK=1` forces GitHub, and local source-checkout installs skip the probe.
|
||||
|
||||
### Changed
|
||||
|
||||
- **CI mirrors repo code to Gitee automatically** (#493; `.github/workflows/mirror-to-gitee.yml`) — the mirror workflow now pushes `main` + tags to the Gitee mirror over HTTPS using `GITEE_TOKEN` (no SSH key), on every push to `main` and every tag, keeping the Gitee `raw/main` install scripts and tags in sync without any manual `git push`. Gated on `GITEE_TOKEN`; skips cleanly when unset.
|
||||
|
||||
## [1.0.40] - 2026-06-24
|
||||
|
||||
This release adds China-accessible install mirrors so the CLI installs reliably from mainland China, where GitHub raw + Releases are slow or fail.
|
||||
@@ -13,6 +25,7 @@ This release adds China-accessible install mirrors so the CLI installs reliably
|
||||
### Added
|
||||
|
||||
- **China mirror via Gitee + npmmirror** (#486; `scripts/install.sh`, `scripts/install.ps1`, `scripts/install-skills.sh`, `scripts/release/sync-to-gitee.sh`, `.github/workflows/release.yml`, `.github/workflows/mirror-to-gitee.yml`) — an opt-in `DWS_GITEE_REPO` env var makes all three installers resolve the latest version and every release asset (binary, `checksums.txt`, `dws-skills.zip`) from the Gitee OpenAPI v5 instead of GitHub; with it unset, installation defaults to GitHub (fully backward compatible). The release pipeline mirrors release attachments to the matching Gitee release after each tag (gated on `GITEE_TOKEN`/`GITEE_REPO`), and a hub-mirror workflow keeps the repo code in sync (gated on `GITEE_PRIVATE_KEY`). README documents three China install channels: Gitee raw script, Gitee release binaries, and the npm package via `registry.npmmirror.com`.
|
||||
- **Skills embedded in the binary** (#488; `skills_embed.go`, `internal/app/skill_setup.go`, `internal/app/skill_setup_embed.go`) — the `skills/` tree (mono + multi) is embedded into the `dws` binary via `go:embed` and `dws skill setup` defaults to the embedded copy, refreshing the installed skill instead of silently reusing a stale copy probed from the current working directory — so skills install offline with no separate download.
|
||||
|
||||
## [1.0.39] - 2026-06-18
|
||||
|
||||
|
||||
@@ -335,6 +335,10 @@ func newDocCreateCommand(runner executor.Runner) *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stripped, ok := stripLeadingDuplicateTitleJSONML(jsonml, name); ok {
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), `note: 正文首个与 --name 相同的一级标题已自动移除(文档标题会单独渲染为页面标题,保留会出现两个标题)。`)
|
||||
jsonml = stripped
|
||||
}
|
||||
createResult, err := docInvocationResult(cmd, runner, "doc", "create_document", params)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -425,6 +429,83 @@ func stripLeadingDuplicateTitleHeading(content, name string) (string, bool) {
|
||||
return strings.TrimLeft(rest, "\r\n"), true
|
||||
}
|
||||
|
||||
// stripLeadingDuplicateTitleJSONML is the JSONML-path counterpart of
|
||||
// stripLeadingDuplicateTitleHeading. The rich-content path (--content-format
|
||||
// jsonml) writes the body verbatim via update_document, so a leading h1 node
|
||||
// whose text equals the document name renders the title twice — exactly the
|
||||
// problem the markdown path already guards against.
|
||||
//
|
||||
// jsonmlBody is the marshaled body produced by prepareDocJSONMLBody: a JSON
|
||||
// array, either a bare node list or wrapped as ["root", {}, ...nodes]. The
|
||||
// first content node is removed only when it is an h1 whose concatenated text
|
||||
// (trimmed, case-insensitive) exactly equals name. Any parse failure or
|
||||
// non-match returns the input untouched, so this never blocks a valid write.
|
||||
func stripLeadingDuplicateTitleJSONML(jsonmlBody, name string) (string, bool) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return jsonmlBody, false
|
||||
}
|
||||
var body []any
|
||||
if err := json.Unmarshal([]byte(jsonmlBody), &body); err != nil {
|
||||
return jsonmlBody, false
|
||||
}
|
||||
// Locate the index of the first content node, skipping a ["root", {}, ...]
|
||||
// wrapper if present.
|
||||
first := 0
|
||||
if len(body) >= 2 {
|
||||
if tag, ok := body[0].(string); ok && tag == "root" {
|
||||
first = 2
|
||||
}
|
||||
}
|
||||
if first >= len(body) {
|
||||
return jsonmlBody, false
|
||||
}
|
||||
node, ok := body[first].([]any)
|
||||
if !ok || len(node) == 0 {
|
||||
return jsonmlBody, false
|
||||
}
|
||||
tag, ok := node[0].(string)
|
||||
if !ok || tag != "h1" {
|
||||
return jsonmlBody, false
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(jsonmlNodeText(node)), name) {
|
||||
return jsonmlBody, false
|
||||
}
|
||||
body = append(body[:first], body[first+1:]...)
|
||||
out, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return jsonmlBody, false
|
||||
}
|
||||
return string(out), true
|
||||
}
|
||||
|
||||
// jsonmlNodeText concatenates all string leaves of a JSONML node, skipping the
|
||||
// leading tag name and any attributes object, so ["h1", {..}, ["span", {..},
|
||||
// ["span", {..}, "Title"]]] yields "Title".
|
||||
func jsonmlNodeText(node any) string {
|
||||
switch v := node.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []any:
|
||||
var b strings.Builder
|
||||
for i, child := range v {
|
||||
// element[0] is the tag name; a map at element[1] is attributes.
|
||||
if i == 0 {
|
||||
if _, isStr := child.(string); isStr {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, isMap := child.(map[string]any); isMap {
|
||||
continue
|
||||
}
|
||||
b.WriteString(jsonmlNodeText(child))
|
||||
}
|
||||
return b.String()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func newDocUpdateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update",
|
||||
|
||||
@@ -159,3 +159,110 @@ func TestDocCreateTitleOnlyContentOmitsMarkdown(t *testing.T) {
|
||||
t.Errorf("markdown param = %v, want omitted", runner.last.Params["markdown"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestDocCreateStripsDuplicateTitleJSONML verifies the end-to-end JSONML path:
|
||||
// `doc create --content-format jsonml` must drop a leading h1 whose text equals
|
||||
// --name before forwarding the body to update_document, otherwise the rich
|
||||
// document shows the page title twice.
|
||||
func TestDocCreateStripsDuplicateTitleJSONML(t *testing.T) {
|
||||
runner := &docCommandRunner{responses: []map[string]any{{"nodeId": "NODE_X"}}}
|
||||
root := newDocTestRoot(runner)
|
||||
|
||||
body := `{"jsonml":[["h1",{},"命令树参考"],["p",{},"正文"]]}`
|
||||
_, errOut, err := executeDocCommand(t, root,
|
||||
"create", "--name", "命令树参考",
|
||||
"--content-format", "jsonml", "--content", body)
|
||||
if err != nil {
|
||||
t.Fatalf("execute: %v\nstderr:\n%s", err, errOut)
|
||||
}
|
||||
if len(runner.all) != 2 {
|
||||
t.Fatalf("calls = %d, want 2 (create + update)", len(runner.all))
|
||||
}
|
||||
if runner.all[1].Tool != "update_document" {
|
||||
t.Fatalf("second tool = %q, want update_document", runner.all[1].Tool)
|
||||
}
|
||||
got, _ := runner.all[1].Params["jsonml"].(string)
|
||||
if strings.Contains(got, "命令树参考") {
|
||||
t.Errorf("jsonml = %q, want duplicate h1 stripped", got)
|
||||
}
|
||||
if !strings.Contains(got, "正文") {
|
||||
t.Errorf("jsonml = %q, want body content kept", got)
|
||||
}
|
||||
if !strings.Contains(errOut, "已自动移除") {
|
||||
t.Errorf("stderr = %q, want a note about the removed heading", errOut)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStripLeadingDuplicateTitleJSONML covers the JSONML-path counterpart: a
|
||||
// leading h1 node whose text equals the document name is removed; everything
|
||||
// else is left untouched.
|
||||
func TestStripLeadingDuplicateTitleJSONML(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
docName string
|
||||
want string
|
||||
stripped bool
|
||||
}{
|
||||
{
|
||||
name: "root-wrapped duplicate h1 stripped",
|
||||
body: `["root",{},["h1",{},"晚会简报"],["p",{},"正文"]]`,
|
||||
docName: "晚会简报",
|
||||
want: `["root",{},["p",{},"正文"]]`,
|
||||
stripped: true,
|
||||
},
|
||||
{
|
||||
name: "nested leaf text matches and stripped",
|
||||
body: `["root",{},["h1",{"uuid":"x"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"晚会简报"]]],["p",{},"正文"]]`,
|
||||
docName: "晚会简报",
|
||||
want: `["root",{},["p",{},"正文"]]`,
|
||||
stripped: true,
|
||||
},
|
||||
{
|
||||
name: "bare body without root wrapper",
|
||||
body: `[["h1",{},"标题"],["p",{},"正文"]]`,
|
||||
docName: "标题",
|
||||
want: `[["p",{},"正文"]]`,
|
||||
stripped: true,
|
||||
},
|
||||
{
|
||||
name: "case insensitive match",
|
||||
body: `["root",{},["h1",{},"Weekly REPORT"],["p",{},"x"]]`,
|
||||
docName: "weekly report",
|
||||
want: `["root",{},["p",{},"x"]]`,
|
||||
stripped: true,
|
||||
},
|
||||
{
|
||||
name: "distinct heading kept",
|
||||
body: `["root",{},["h1",{},"背景"],["p",{},"正文"]]`,
|
||||
docName: "晚会简报",
|
||||
want: `["root",{},["h1",{},"背景"],["p",{},"正文"]]`,
|
||||
stripped: false,
|
||||
},
|
||||
{
|
||||
name: "non-h1 leading node kept",
|
||||
body: `["root",{},["h2",{},"晚会简报"],["p",{},"正文"]]`,
|
||||
docName: "晚会简报",
|
||||
want: `["root",{},["h2",{},"晚会简报"],["p",{},"正文"]]`,
|
||||
stripped: false,
|
||||
},
|
||||
{
|
||||
name: "invalid json untouched",
|
||||
body: `not json`,
|
||||
docName: "x",
|
||||
want: `not json`,
|
||||
stripped: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, ok := stripLeadingDuplicateTitleJSONML(tc.body, tc.docName)
|
||||
if ok != tc.stripped {
|
||||
t.Fatalf("stripped = %v, want %v", ok, tc.stripped)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("body = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ set -eu
|
||||
REPO="DingTalk-Real-AI/dingtalk-workspace-cli"
|
||||
# China mirror: Gitee repo "owner/repo". When set, version + asset URLs resolve via Gitee API.
|
||||
GITEE_REPO="${DWS_GITEE_REPO:-}"
|
||||
# Auto-fallback Gitee mirror used when GitHub is unreachable (see pick_source).
|
||||
GITEE_FALLBACK_REPO="${DWS_GITEE_FALLBACK_REPO:-DingTalk-Real-AI/dingtalk-workspace-cli}"
|
||||
VERSION="${DWS_VERSION:-latest}"
|
||||
SKILL_NAME="dws"
|
||||
ROOT="${DWS_SKILLS_ROOT:-$PWD}"
|
||||
@@ -48,6 +50,14 @@ gitee_api() {
|
||||
return 1
|
||||
}
|
||||
|
||||
pick_source() {
|
||||
[ -n "$GITEE_REPO" ] && return 0
|
||||
[ "${DWS_NO_FALLBACK:-0}" = "1" ] && return 0
|
||||
curl -fsS --connect-timeout 5 --max-time 12 -o /dev/null "https://github.com/${REPO}/releases/latest" 2>/dev/null && return 0
|
||||
GITEE_REPO="$GITEE_FALLBACK_REPO"
|
||||
printf ' ⚠ GitHub 不可达,自动切换国内 Gitee 镜像: %s\n' "$GITEE_REPO"
|
||||
}
|
||||
|
||||
resolve_version() {
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
if [ -n "$GITEE_REPO" ]; then
|
||||
@@ -197,6 +207,7 @@ install_skills_to_root() {
|
||||
|
||||
main() {
|
||||
need_cmd curl
|
||||
pick_source
|
||||
resolve_version
|
||||
|
||||
printf '\n'
|
||||
|
||||
@@ -29,6 +29,8 @@ $Repo = "DingTalk-Real-AI/dingtalk-workspace-cli"
|
||||
$BinName = "dws"
|
||||
# China mirror: Gitee repo "owner/repo". When set, version + asset URLs resolve via the Gitee API.
|
||||
$GiteeRepo = if ($env:DWS_GITEE_REPO) { $env:DWS_GITEE_REPO } else { "" }
|
||||
# Auto-fallback Gitee mirror used when GitHub is unreachable (see Resolve-Source).
|
||||
$GiteeFallbackRepo = if ($env:DWS_GITEE_FALLBACK_REPO) { $env:DWS_GITEE_FALLBACK_REPO } else { "DingTalk-Real-AI/dingtalk-workspace-cli" }
|
||||
$InstallDir = if ($env:DWS_INSTALL_DIR) { $env:DWS_INSTALL_DIR } else { Join-Path $HOME ".local\bin" }
|
||||
$Version = if ($env:DWS_VERSION) { $env:DWS_VERSION } else { "latest" }
|
||||
$NoSkills = $env:DWS_NO_SKILLS -eq "1"
|
||||
@@ -146,6 +148,20 @@ function Get-GiteeAssetUrl {
|
||||
return ""
|
||||
}
|
||||
|
||||
function Resolve-Source {
|
||||
# Explicit DWS_GITEE_REPO wins; else probe GitHub and fall back to Gitee when unreachable.
|
||||
if ($GiteeRepo -ne "") { return }
|
||||
if ($env:DWS_NO_FALLBACK -eq "1") { return }
|
||||
try {
|
||||
Invoke-WebRequest -Uri "https://github.com/$Repo/releases/latest" -Method Head `
|
||||
-TimeoutSec 12 -UseBasicParsing -ErrorAction Stop 2>$null | Out-Null
|
||||
return
|
||||
} catch {
|
||||
$script:GiteeRepo = $GiteeFallbackRepo
|
||||
Write-Say "⚠ GitHub 不可达,自动切换国内 Gitee 镜像: $script:GiteeRepo"
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-LatestVersion {
|
||||
if ($Version -eq "latest") {
|
||||
if ($GiteeRepo -ne "") {
|
||||
@@ -591,6 +607,10 @@ $SourceRoot = Resolve-SourceRoot
|
||||
|
||||
Write-Banner
|
||||
|
||||
# Pick GitHub vs Gitee mirror (auto-fallback when GitHub is unreachable).
|
||||
# Skipped when installing from a local source checkout (no download needed).
|
||||
if (-not $SourceRoot) { Resolve-Source }
|
||||
|
||||
if (!$NoSkills) {
|
||||
Resolve-SkillMode
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ BIN_NAME="dws"
|
||||
# the Gitee API (https://gitee.com/api/v5) instead of GitHub. Gitee attachment URLs
|
||||
# carry an unstable numeric id, so every asset is resolved by name at install time.
|
||||
GITEE_REPO="${DWS_GITEE_REPO:-}"
|
||||
# Auto-fallback: when DWS_GITEE_REPO is not set, the installer probes GitHub and,
|
||||
# if it is unreachable (typical in mainland China), automatically switches to this
|
||||
# Gitee mirror — so a plain `curl … | sh` works everywhere with no env var.
|
||||
# Set DWS_NO_FALLBACK=1 to disable the probe and force GitHub.
|
||||
GITEE_FALLBACK_REPO="${DWS_GITEE_FALLBACK_REPO:-DingTalk-Real-AI/dingtalk-workspace-cli}"
|
||||
INSTALL_DIR="${DWS_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
INSTALL_NAME="${DWS_INSTALL_NAME:-$BIN_NAME}"
|
||||
VERSION="${DWS_VERSION:-latest}"
|
||||
@@ -153,6 +158,21 @@ detect_arch() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Decide the download source. An explicit DWS_GITEE_REPO always wins. Otherwise
|
||||
# probe GitHub Releases; if it is unreachable (typical in mainland China), switch
|
||||
# GITEE_REPO to the mirror so every subsequent resolve/download uses Gitee.
|
||||
pick_source() {
|
||||
[ -n "$GITEE_REPO" ] && return 0
|
||||
[ "${DWS_NO_FALLBACK:-0}" = "1" ] && return 0
|
||||
if need_cmd curl; then
|
||||
curl -fsS --connect-timeout 5 --max-time 12 -o /dev/null "https://github.com/${REPO}/releases/latest" 2>/dev/null && return 0
|
||||
elif need_cmd wget; then
|
||||
wget -q --timeout=12 --tries=1 -O /dev/null "https://github.com/${REPO}/releases/latest" 2>/dev/null && return 0
|
||||
fi
|
||||
GITEE_REPO="$GITEE_FALLBACK_REPO"
|
||||
say "⚠ GitHub 不可达,自动切换国内 Gitee 镜像: ${GITEE_REPO}"
|
||||
}
|
||||
|
||||
# Resolve the latest version tag from GitHub
|
||||
resolve_version() {
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
@@ -613,6 +633,10 @@ main() {
|
||||
|
||||
print_banner
|
||||
|
||||
# Pick GitHub vs Gitee mirror (auto-fallback when GitHub is unreachable).
|
||||
# Skipped when installing from a local source checkout (no download needed).
|
||||
[ -z "$source_root" ] && pick_source
|
||||
|
||||
# Resolve skill mode only when we are actually going to touch skills.
|
||||
if [ "$NO_SKILLS" != "1" ]; then
|
||||
resolve_skill_mode
|
||||
|
||||
Reference in New Issue
Block a user