@yyy9527/openclaw-manager 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
+ CONFIG_FILE="${OPENCLAW_CONFIG_FILE:-${PROJECT_ROOT}/config/openclaw.env}"
7
+ ENV_FILE="${OPENCLAW_DOTENV_FILE:-${PROJECT_ROOT}/.env}"
8
+
9
+ log() {
10
+ printf '[set-qwen] %s\n' "$*"
11
+ }
12
+
13
+ fail() {
14
+ printf '[set-qwen][ERROR] %s\n' "$*" >&2
15
+ exit 1
16
+ }
17
+
18
+ load_env() {
19
+ [ -f "${CONFIG_FILE}" ] || fail "缺少配置文件:${CONFIG_FILE}"
20
+ [ -f "${ENV_FILE}" ] || fail "缺少环境文件:${ENV_FILE}"
21
+ set -a
22
+ # shellcheck source=/dev/null
23
+ source "${CONFIG_FILE}"
24
+ # shellcheck source=/dev/null
25
+ source "${ENV_FILE}"
26
+ set +a
27
+ }
28
+
29
+ escape_env_value() {
30
+ local raw="$1"
31
+ raw="${raw//\\/\\\\}"
32
+ raw="${raw//\"/\\\"}"
33
+ printf '"%s"' "${raw}"
34
+ }
35
+
36
+ upsert_env_key() {
37
+ local file="$1"
38
+ local key="$2"
39
+ local value="$3"
40
+ local escaped
41
+ local tmp_file
42
+ escaped="$(escape_env_value "${value}")"
43
+ tmp_file="$(mktemp)"
44
+
45
+ if [ -f "${file}" ]; then
46
+ awk -v key="${key}" -v val="${escaped}" '
47
+ BEGIN { updated = 0 }
48
+ $0 ~ "^[[:space:]]*" key "=" && updated == 0 {
49
+ print key "=" val
50
+ updated = 1
51
+ next
52
+ }
53
+ { print }
54
+ END {
55
+ if (updated == 0) {
56
+ print key "=" val
57
+ }
58
+ }
59
+ ' "${file}" > "${tmp_file}"
60
+ else
61
+ {
62
+ printf '# 由 set-model:qwen 生成;可按实例补充自定义键\n'
63
+ printf '%s=%s\n' "${key}" "${escaped}"
64
+ } > "${tmp_file}"
65
+ fi
66
+ mv "${tmp_file}" "${file}"
67
+ }
68
+
69
+ resolve_template_file() {
70
+ local id="$1"
71
+ local template_dir="${PROJECT_ROOT}/config/model-env-templates"
72
+ local candidate_env="${template_dir}/${id}.env"
73
+ local candidate_example="${template_dir}/${id}.env.example"
74
+ if [ -f "${candidate_env}" ]; then
75
+ printf '%s\n' "${candidate_env}"
76
+ return 0
77
+ fi
78
+ if [ -f "${candidate_example}" ]; then
79
+ printf '%s\n' "${candidate_example}"
80
+ return 0
81
+ fi
82
+ return 1
83
+ }
84
+
85
+ write_instance_model_env() {
86
+ local id="$1"
87
+ local home_dir="${OPENCLAW_HOME_ROOT}/${id}"
88
+ local model_env="${home_dir}/model.env"
89
+ local template_file
90
+
91
+ MODEL_PROVIDER_ID="openai"
92
+ MODEL_PRIMARY="openai/${QWEN_MODEL:-qwen-max}"
93
+ MODEL_BASE_URL="${QWEN_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1}"
94
+ MODEL_API_KEY_ENV="${QWEN_API_KEY_ENV:-DASHSCOPE_API_KEY}"
95
+ MODEL_API="openai-completions"
96
+ MODEL_ID="${QWEN_MODEL:-qwen-max}"
97
+ MODEL_NAME="Qwen Vendor"
98
+ MODEL_CONTEXT_WINDOW=131072
99
+ MODEL_MAX_TOKENS=8192
100
+
101
+ if template_file="$(resolve_template_file "${id}")"; then
102
+ # shellcheck source=/dev/null
103
+ source "${template_file}"
104
+ log "实例 ${id} 使用模板:${template_file}"
105
+ fi
106
+
107
+ mkdir -p "${home_dir}"
108
+ if [ ! -f "${model_env}" ]; then
109
+ printf '# 由 set-model:qwen 生成;可按实例补充自定义键\n' > "${model_env}"
110
+ fi
111
+
112
+ # 仅更新受控键,避免覆盖用户自定义键。
113
+ upsert_env_key "${model_env}" "MODEL_PROVIDER_ID" "${MODEL_PROVIDER_ID}"
114
+ upsert_env_key "${model_env}" "MODEL_PRIMARY" "${MODEL_PRIMARY}"
115
+ upsert_env_key "${model_env}" "MODEL_BASE_URL" "${MODEL_BASE_URL}"
116
+ upsert_env_key "${model_env}" "MODEL_API_KEY_ENV" "${MODEL_API_KEY_ENV}"
117
+ upsert_env_key "${model_env}" "MODEL_API" "${MODEL_API}"
118
+ upsert_env_key "${model_env}" "MODEL_ID" "${MODEL_ID}"
119
+ upsert_env_key "${model_env}" "MODEL_NAME" "${MODEL_NAME}"
120
+ upsert_env_key "${model_env}" "MODEL_CONTEXT_WINDOW" "${MODEL_CONTEXT_WINDOW}"
121
+ upsert_env_key "${model_env}" "MODEL_MAX_TOKENS" "${MODEL_MAX_TOKENS}"
122
+ log "实例 ${id} 模型配置已安全更新:${model_env}"
123
+ }
124
+
125
+ main() {
126
+ load_env
127
+ [ -n "${OPENCLAW_INSTANCE_IDS:-}" ] || fail "OPENCLAW_INSTANCE_IDS 未配置"
128
+ if [ -z "${OPENCLAW_HOME_ROOT:-}" ]; then
129
+ OPENCLAW_HOME_ROOT="${OPENCLAW_DATA_ROOT:-${HOME}/openclaws}"
130
+ fi
131
+ [ -n "${QWEN_API_KEY:-${DASHSCOPE_API_KEY:-}}" ] || fail "缺少 QWEN_API_KEY 或 DASHSCOPE_API_KEY"
132
+
133
+ IFS=',' read -r -a instance_list <<<"${OPENCLAW_INSTANCE_IDS}"
134
+ for id in "${instance_list[@]}"; do
135
+ write_instance_model_env "${id}"
136
+ done
137
+
138
+ bash "${PROJECT_ROOT}/scripts/sync_model_configs.sh"
139
+ log "Qwen 厂商模型配置已写入各实例家目录。执行 npm run apply-config 使其生效。"
140
+ }
141
+
142
+ main "$@"
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
+ CONFIG_FILE="${OPENCLAW_CONFIG_FILE:-${PROJECT_ROOT}/config/openclaw.env}"
7
+ ENV_FILE="${OPENCLAW_DOTENV_FILE:-${PROJECT_ROOT}/.env}"
8
+
9
+ log() {
10
+ printf '[setup_env] %s\n' "$*"
11
+ }
12
+
13
+ warn() {
14
+ printf '[setup_env][WARN] %s\n' "$*" >&2
15
+ }
16
+
17
+ require_cmd() {
18
+ local cmd="$1"
19
+ local install_hint="$2"
20
+ if command -v "${cmd}" >/dev/null 2>&1; then
21
+ log "已检测到 ${cmd}"
22
+ else
23
+ warn "未检测到 ${cmd}。建议执行:${install_hint}"
24
+ return 1
25
+ fi
26
+ }
27
+
28
+ install_with_brew_if_missing() {
29
+ local formula="$1"
30
+ local check_cmd="$2"
31
+ if command -v "${check_cmd}" >/dev/null 2>&1; then
32
+ log "${formula} 已存在,跳过安装"
33
+ return 0
34
+ fi
35
+ log "通过 Homebrew 安装 ${formula}"
36
+ brew install "${formula}"
37
+ }
38
+
39
+ install_pnpm_if_missing() {
40
+ if command -v pnpm >/dev/null 2>&1; then
41
+ log "pnpm 已存在,跳过安装"
42
+ return 0
43
+ fi
44
+ log "通过 npm 安装 pnpm"
45
+ npm install -g pnpm
46
+ }
47
+
48
+ ensure_pnpm_home() {
49
+ export PNPM_HOME="${PNPM_HOME:-${HOME}/Library/pnpm}"
50
+ mkdir -p "${PNPM_HOME}"
51
+ case ":${PATH}:" in
52
+ *":${PNPM_HOME}:"*) ;;
53
+ *) export PATH="${PNPM_HOME}:${PATH}" ;;
54
+ esac
55
+ }
56
+
57
+ install_clawhub_with_pnpm_if_missing() {
58
+ if command -v clawhub >/dev/null 2>&1; then
59
+ log "clawhub 已存在,跳过安装"
60
+ return 0
61
+ fi
62
+ log "通过 pnpm 全局安装 clawhub(失败将自动重试)"
63
+ local max_retries=3
64
+ local attempt=1
65
+ while [ "${attempt}" -le "${max_retries}" ]; do
66
+ if pnpm add -g clawhub; then
67
+ return 0
68
+ fi
69
+ warn "pnpm 安装 clawhub 第 ${attempt}/${max_retries} 次失败,准备重试。"
70
+ sleep 2
71
+ attempt=$((attempt + 1))
72
+ done
73
+ warn "pnpm 安装 clawhub 多次失败,请手动执行:pnpm add -g clawhub"
74
+ }
75
+
76
+ install_cask_with_brew_if_missing() {
77
+ local cask="$1"
78
+ local app_check="$2"
79
+ if [ -e "${app_check}" ]; then
80
+ log "${cask} 已存在,跳过安装"
81
+ return 0
82
+ fi
83
+ log "通过 Homebrew Cask 安装 ${cask}"
84
+ brew install --cask "${cask}"
85
+ }
86
+
87
+ copy_template_if_missing() {
88
+ local src="$1"
89
+ local dst="$2"
90
+ if [ -f "${dst}" ]; then
91
+ log "${dst} 已存在,跳过"
92
+ return 0
93
+ fi
94
+ mkdir -p "$(dirname "${dst}")"
95
+ cp "${src}" "${dst}"
96
+ log "已创建 ${dst}"
97
+ }
98
+
99
+ main() {
100
+ log "开始初始化 macOS 环境(幂等执行)"
101
+
102
+ if [[ "$(uname -s)" != "Darwin" ]]; then
103
+ warn "当前系统不是 macOS,脚本可能不适用。"
104
+ fi
105
+
106
+ if ! command -v brew >/dev/null 2>&1; then
107
+ warn "未检测到 Homebrew,请先安装:"
108
+ warn "/bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
109
+ exit 1
110
+ fi
111
+
112
+ install_with_brew_if_missing "node" "node"
113
+ install_with_brew_if_missing "jq" "jq"
114
+ install_with_brew_if_missing "yq" "yq"
115
+ install_with_brew_if_missing "gh" "gh"
116
+ install_cask_with_brew_if_missing "docker" "/Applications/Docker.app"
117
+ install_with_brew_if_missing "ollama" "ollama"
118
+
119
+ install_pnpm_if_missing
120
+ ensure_pnpm_home
121
+ install_clawhub_with_pnpm_if_missing
122
+
123
+ require_cmd "node" "brew install node"
124
+ require_cmd "npm" "brew install node"
125
+ require_cmd "pnpm" "npm install -g pnpm"
126
+ require_cmd "docker" "brew install --cask docker"
127
+ require_cmd "ollama" "brew install ollama"
128
+
129
+ if ! docker info >/dev/null 2>&1; then
130
+ warn "Docker Desktop 似乎未启动,请手动启动后重试脚本。"
131
+ exit 1
132
+ fi
133
+ log "Docker daemon 状态正常"
134
+
135
+ copy_template_if_missing "${PROJECT_ROOT}/config/openclaw.env.example" "${CONFIG_FILE}"
136
+ copy_template_if_missing "${PROJECT_ROOT}/.env.example" "${ENV_FILE}"
137
+
138
+ set -a
139
+ # shellcheck source=/dev/null
140
+ source "${CONFIG_FILE}"
141
+ set +a
142
+
143
+ if [ -z "${OPENCLAW_HOME_ROOT:-}" ]; then
144
+ OPENCLAW_HOME_ROOT="${OPENCLAW_DATA_ROOT:-${HOME}/openclaws}"
145
+ fi
146
+
147
+ mkdir -p "${OPENCLAW_HOME_ROOT}" "${PROJECT_ROOT}/logs"
148
+ log "已确保实例家目录根路径存在:${OPENCLAW_HOME_ROOT}"
149
+ log "已确保 logs 目录存在"
150
+
151
+ "${PROJECT_ROOT}/scripts/skills_sync.sh"
152
+
153
+ log "环境初始化完成"
154
+ }
155
+
156
+ main "$@"
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
+ CONFIG_FILE="${OPENCLAW_CONFIG_FILE:-${PROJECT_ROOT}/config/openclaw.env}"
7
+
8
+ log() {
9
+ printf '[skills-sync] %s\n' "$*"
10
+ }
11
+
12
+ warn() {
13
+ printf '[skills-sync][WARN] %s\n' "$*" >&2
14
+ }
15
+
16
+ load_env() {
17
+ [ -f "${CONFIG_FILE}" ] || {
18
+ warn "缺少配置文件:${CONFIG_FILE}"
19
+ exit 1
20
+ }
21
+ set -a
22
+ # shellcheck source=/dev/null
23
+ source "${CONFIG_FILE}"
24
+ set +a
25
+ }
26
+
27
+ install_slug_with_retry() {
28
+ local slug="$1"
29
+ local max_retries="${OPENCLAW_CLAWHUB_INSTALL_RETRIES:-4}"
30
+ local attempt=1
31
+ while [ "${attempt}" -le "${max_retries}" ]; do
32
+ local output
33
+ output="$(clawhub --no-input --workdir "${OPENCLAW_HOME_ROOT}/shared" --dir skills install "${slug}" 2>&1 || true)"
34
+ if printf '%s' "${output}" | awk '/Installed|Up to date/{found=1} END{exit found?0:1}'; then
35
+ return 0
36
+ fi
37
+ if printf '%s' "${output}" | awk '/Rate limit exceeded|Too many requests/{found=1} END{exit found?0:1}'; then
38
+ warn "clawhub install ${slug} 命中限流,第 ${attempt}/${max_retries} 次,退避后重试。"
39
+ sleep $((attempt * 5))
40
+ else
41
+ warn "clawhub install ${slug} 第 ${attempt}/${max_retries} 次失败:$(printf '%s' "${output}" | tr '\n' ' ' | cut -c1-140)"
42
+ sleep 2
43
+ fi
44
+ attempt=$((attempt + 1))
45
+ done
46
+ return 1
47
+ }
48
+
49
+ resolve_best_slug() {
50
+ local query="$1"
51
+ local result
52
+ result="$(clawhub --no-input search "${query}" 2>/dev/null || true)"
53
+ printf '%s\n' "${result}" | awk 'NR>1 && $1 !~ /^-/ {print $1; exit}'
54
+ }
55
+
56
+ search_slugs() {
57
+ local query="$1"
58
+ local result
59
+ result="$(clawhub --no-input search "${query}" 2>/dev/null || true)"
60
+ printf '%s\n' "${result}" | awk 'NR>1 && $1 !~ /^-/ {print $1}'
61
+ }
62
+
63
+ ensure_dirs() {
64
+ if [ -z "${OPENCLAW_HOME_ROOT:-}" ]; then
65
+ OPENCLAW_HOME_ROOT="${OPENCLAW_DATA_ROOT:-${HOME}/openclaws}"
66
+ fi
67
+ OPENCLAW_SHARED_SKILLS_DIR="${OPENCLAW_SHARED_SKILLS_DIR:-${OPENCLAW_HOME_ROOT}/shared/skills}"
68
+
69
+ mkdir -p "${OPENCLAW_SHARED_SKILLS_DIR}"
70
+ log "共享技能目录:${OPENCLAW_SHARED_SKILLS_DIR}"
71
+
72
+ IFS=',' read -r -a instance_list <<<"${OPENCLAW_INSTANCE_IDS}"
73
+ for id in "${instance_list[@]}"; do
74
+ local_home="${OPENCLAW_HOME_ROOT}/${id}"
75
+ local_skills="${local_home}/skills"
76
+ mkdir -p "${local_skills}"
77
+ printf "instance=%s\n" "${id}" > "${local_home}/.openclaw-home"
78
+ log "实例技能目录:${local_skills}"
79
+ done
80
+ }
81
+
82
+ install_from_clawhub() {
83
+ local skills_file="${PROJECT_ROOT}/config/clawhub-skills.txt"
84
+ local enabled="${OPENCLAW_ENABLE_CLAWHUB_SKILLS:-true}"
85
+ if [ "${enabled}" != "true" ]; then
86
+ log "已禁用 clawhub 自动安装,跳过"
87
+ return 0
88
+ fi
89
+
90
+ if ! command -v clawhub >/dev/null 2>&1; then
91
+ warn "未检测到 clawhub 命令,跳过技能自动安装。"
92
+ return 0
93
+ fi
94
+
95
+ if ! clawhub whoami >/dev/null 2>&1; then
96
+ warn "clawhub 未登录,可能触发限流导致安装失败。建议先执行:npm run clawhub:login"
97
+ fi
98
+
99
+ if [ ! -f "${skills_file}" ]; then
100
+ warn "未找到 ${skills_file},跳过技能自动安装。"
101
+ return 0
102
+ fi
103
+
104
+ log "优先安装 find-skills 技能用于检索能力扩展"
105
+ if install_slug_with_retry "find-skills"; then
106
+ log "find-skills 安装成功"
107
+ else
108
+ warn "find-skills 安装失败,后续继续搜索但不自动安装不确定技能。"
109
+ fi
110
+
111
+ while IFS= read -r skill || [ -n "${skill}" ]; do
112
+ skill="$(printf '%s' "${skill}" | xargs)"
113
+ [ -z "${skill}" ] && continue
114
+ [[ "${skill}" =~ ^# ]] && continue
115
+
116
+ log "通过 find-skills 流程检索:${skill}"
117
+ local normalized
118
+ normalized="$(printf '%s' "${skill}" | tr ' ' '-')"
119
+ local slugs
120
+ slugs="$(search_slugs "${skill}")"
121
+ if [ -z "${slugs}" ]; then
122
+ warn "未搜索到可用技能:${skill}"
123
+ continue
124
+ fi
125
+
126
+ local matched=""
127
+ while IFS= read -r slug; do
128
+ if [ "${slug}" = "${normalized}" ] || [ "${slug}" = "${skill}" ]; then
129
+ matched="${slug}"
130
+ break
131
+ fi
132
+ done <<< "${slugs}"
133
+
134
+ if [ -n "${matched}" ]; then
135
+ log "命中精确 slug,开始安装:${matched}"
136
+ if ! install_slug_with_retry "${matched}"; then
137
+ warn "技能 ${matched} 安装失败,请稍后重试。"
138
+ fi
139
+ continue
140
+ fi
141
+
142
+ warn "未找到精确匹配 slug(${normalized}),为避免误装已跳过自动安装。"
143
+ warn "建议候选(前3):$(printf '%s\n' "${slugs}" | head -n 3 | tr '\n' ' ')"
144
+ done < "${skills_file}"
145
+ }
146
+
147
+ main() {
148
+ load_env
149
+ ensure_dirs
150
+ install_from_clawhub
151
+ log "技能目录同步完成"
152
+ }
153
+
154
+ main "$@"
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
+ CONFIG_FILE="${OPENCLAW_CONFIG_FILE:-${PROJECT_ROOT}/config/openclaw.env}"
7
+ ENV_FILE="${OPENCLAW_DOTENV_FILE:-${PROJECT_ROOT}/.env}"
8
+
9
+ log() {
10
+ printf '[sync-model] %s\n' "$*"
11
+ }
12
+
13
+ warn() {
14
+ printf '[sync-model][WARN] %s\n' "$*" >&2
15
+ }
16
+
17
+ load_env() {
18
+ [ -f "${CONFIG_FILE}" ] || {
19
+ warn "缺少配置文件:${CONFIG_FILE}"
20
+ exit 1
21
+ }
22
+ set -a
23
+ # shellcheck source=/dev/null
24
+ source "${CONFIG_FILE}"
25
+ if [ -f "${ENV_FILE}" ]; then
26
+ # shellcheck source=/dev/null
27
+ source "${ENV_FILE}"
28
+ fi
29
+ set +a
30
+ }
31
+
32
+ escape_env_value() {
33
+ local raw="$1"
34
+ raw="${raw//\\/\\\\}"
35
+ raw="${raw//\"/\\\"}"
36
+ printf '"%s"' "${raw}"
37
+ }
38
+
39
+ upsert_env_key() {
40
+ local file="$1"
41
+ local key="$2"
42
+ local value="$3"
43
+ local escaped
44
+ local tmp_file
45
+ escaped="$(escape_env_value "${value}")"
46
+ tmp_file="$(mktemp)"
47
+
48
+ if [ -f "${file}" ]; then
49
+ awk -v key="${key}" -v val="${escaped}" '
50
+ BEGIN { updated = 0 }
51
+ $0 ~ "^[[:space:]]*" key "=" && updated == 0 {
52
+ print key "=" val
53
+ updated = 1
54
+ next
55
+ }
56
+ { print }
57
+ END {
58
+ if (updated == 0) {
59
+ print key "=" val
60
+ }
61
+ }
62
+ ' "${file}" > "${tmp_file}"
63
+ else
64
+ {
65
+ printf '# 每个实例独立模型配置(修改后执行 npm run apply-config 生效)\n'
66
+ printf '%s=%s\n' "${key}" "${escaped}"
67
+ } > "${tmp_file}"
68
+ fi
69
+ mv "${tmp_file}" "${file}"
70
+ }
71
+
72
+ resolve_template_file() {
73
+ local id="$1"
74
+ local template_dir="${PROJECT_ROOT}/config/model-env-templates"
75
+ local candidate_env="${template_dir}/${id}.env"
76
+ local candidate_example="${template_dir}/${id}.env.example"
77
+ if [ -f "${candidate_env}" ]; then
78
+ printf '%s\n' "${candidate_env}"
79
+ return 0
80
+ fi
81
+ if [ -f "${candidate_example}" ]; then
82
+ printf '%s\n' "${candidate_example}"
83
+ return 0
84
+ fi
85
+ return 1
86
+ }
87
+
88
+ ensure_default_model_env() {
89
+ local instance_id="$1"
90
+ local home_dir="${OPENCLAW_HOME_ROOT}/${instance_id}"
91
+ local model_env="${home_dir}/model.env"
92
+ local template_file
93
+
94
+ mkdir -p "${home_dir}"
95
+ if [ -f "${model_env}" ]; then
96
+ return 0
97
+ fi
98
+ printf '# 每个实例独立模型配置(修改后执行 npm run apply-config 生效)\n' > "${model_env}"
99
+
100
+ MODEL_PROVIDER_ID="openai"
101
+ MODEL_PRIMARY="openai/${QWEN_MODEL:-qwen-max}"
102
+ MODEL_BASE_URL="${QWEN_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1}"
103
+ MODEL_API_KEY_ENV="${QWEN_API_KEY_ENV:-DASHSCOPE_API_KEY}"
104
+ MODEL_API="openai-completions"
105
+ MODEL_ID="${QWEN_MODEL:-qwen-max}"
106
+ MODEL_NAME="Qwen Vendor"
107
+ MODEL_CONTEXT_WINDOW=131072
108
+ MODEL_MAX_TOKENS=8192
109
+
110
+ if template_file="$(resolve_template_file "${instance_id}")"; then
111
+ # shellcheck source=/dev/null
112
+ source "${template_file}"
113
+ log "实例 ${instance_id} 使用模板:${template_file}"
114
+ fi
115
+
116
+ # 仅补齐受控键,避免覆盖用户追加的自定义配置。
117
+ upsert_env_key "${model_env}" "MODEL_PROVIDER_ID" "${MODEL_PROVIDER_ID}"
118
+ upsert_env_key "${model_env}" "MODEL_PRIMARY" "${MODEL_PRIMARY}"
119
+ upsert_env_key "${model_env}" "MODEL_BASE_URL" "${MODEL_BASE_URL}"
120
+ upsert_env_key "${model_env}" "MODEL_API_KEY_ENV" "${MODEL_API_KEY_ENV}"
121
+ upsert_env_key "${model_env}" "MODEL_API" "${MODEL_API}"
122
+ upsert_env_key "${model_env}" "MODEL_ID" "${MODEL_ID}"
123
+ upsert_env_key "${model_env}" "MODEL_NAME" "${MODEL_NAME}"
124
+ upsert_env_key "${model_env}" "MODEL_CONTEXT_WINDOW" "${MODEL_CONTEXT_WINDOW}"
125
+ upsert_env_key "${model_env}" "MODEL_MAX_TOKENS" "${MODEL_MAX_TOKENS}"
126
+ log "已生成默认模型配置:${model_env}"
127
+ }
128
+
129
+ sync_instance_model() {
130
+ local instance_id="$1"
131
+ local home_dir="${OPENCLAW_HOME_ROOT}/${instance_id}"
132
+ local model_env="${home_dir}/model.env"
133
+ local openclaw_json="${home_dir}/openclaw.json"
134
+ local resolved_api_key=""
135
+
136
+ ensure_default_model_env "${instance_id}"
137
+
138
+ set -a
139
+ # shellcheck source=/dev/null
140
+ source "${model_env}"
141
+ set +a
142
+
143
+ : "${MODEL_PROVIDER_ID:?MODEL_PROVIDER_ID 未配置}"
144
+ : "${MODEL_PRIMARY:?MODEL_PRIMARY 未配置}"
145
+ : "${MODEL_BASE_URL:?MODEL_BASE_URL 未配置}"
146
+ : "${MODEL_API_KEY_ENV:?MODEL_API_KEY_ENV 未配置}"
147
+ : "${MODEL_API:?MODEL_API 未配置}"
148
+ : "${MODEL_ID:?MODEL_ID 未配置}"
149
+ : "${MODEL_NAME:?MODEL_NAME 未配置}"
150
+ : "${MODEL_CONTEXT_WINDOW:?MODEL_CONTEXT_WINDOW 未配置}"
151
+ : "${MODEL_MAX_TOKENS:?MODEL_MAX_TOKENS 未配置}"
152
+
153
+ # MODEL_API_KEY_ENV 兼容两种写法:
154
+ # 1) 环境变量名(推荐),例如 DASHSCOPE_API_KEY
155
+ # 2) 直接填写 API Key(不推荐,但兼容历史配置)
156
+ if [ -n "${!MODEL_API_KEY_ENV:-}" ]; then
157
+ resolved_api_key="${!MODEL_API_KEY_ENV}"
158
+ else
159
+ resolved_api_key="${MODEL_API_KEY_ENV}"
160
+ fi
161
+
162
+ if [ -z "${resolved_api_key}" ]; then
163
+ warn "实例 ${instance_id} 未解析到可用 API Key(MODEL_API_KEY_ENV=${MODEL_API_KEY_ENV})"
164
+ exit 1
165
+ fi
166
+
167
+ [ -f "${openclaw_json}" ] || printf '{}\n' > "${openclaw_json}"
168
+ local tmp_file
169
+ tmp_file="$(mktemp)"
170
+
171
+ jq \
172
+ --arg provider_id "${MODEL_PROVIDER_ID}" \
173
+ --arg primary "${MODEL_PRIMARY}" \
174
+ --arg base_url "${MODEL_BASE_URL}" \
175
+ --arg api_key "${resolved_api_key}" \
176
+ --arg api_type "${MODEL_API}" \
177
+ --arg model_id "${MODEL_ID}" \
178
+ --arg model_name "${MODEL_NAME}" \
179
+ --argjson context_window "${MODEL_CONTEXT_WINDOW}" \
180
+ --argjson max_tokens "${MODEL_MAX_TOKENS}" \
181
+ '
182
+ .agents.defaults.model.primary = $primary
183
+ | .models.providers[$provider_id].baseUrl = $base_url
184
+ | .models.providers[$provider_id].apiKey = $api_key
185
+ | .models.providers[$provider_id].api = $api_type
186
+ | .models.providers[$provider_id].models = [
187
+ {
188
+ id: $model_id,
189
+ name: $model_name,
190
+ reasoning: false,
191
+ input: ["text"],
192
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
193
+ contextWindow: $context_window,
194
+ maxTokens: $max_tokens
195
+ }
196
+ ]
197
+ ' "${openclaw_json}" > "${tmp_file}"
198
+
199
+ mv "${tmp_file}" "${openclaw_json}"
200
+ log "实例 ${instance_id} 模型配置已同步到家目录"
201
+ }
202
+
203
+ main() {
204
+ load_env
205
+ if [ -z "${OPENCLAW_HOME_ROOT:-}" ]; then
206
+ OPENCLAW_HOME_ROOT="${OPENCLAW_DATA_ROOT:-${HOME}/openclaws}"
207
+ fi
208
+ : "${OPENCLAW_INSTANCE_IDS:?OPENCLAW_INSTANCE_IDS 未配置}"
209
+
210
+ IFS=',' read -r -a instance_list <<<"${OPENCLAW_INSTANCE_IDS}"
211
+ for id in "${instance_list[@]}"; do
212
+ sync_instance_model "${id}"
213
+ done
214
+ log "全部实例模型配置同步完成"
215
+ }
216
+
217
+ main "$@"