Skip to content

Commit 2c2e23e

Browse files
committed
chore: add openclaw init script
1 parent 89f8fc7 commit 2c2e23e

1 file changed

Lines changed: 354 additions & 0 deletions

File tree

apps/openclaw/src/init.js

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
#!/usr/bin/env node
2+
/**
3+
* OpenClaw startup initializer for Docker / Websoft9.
4+
*
5+
* Runs BEFORE the gateway starts. Auto-configures AI providers based on
6+
* environment variables — no CLI commands needed for users.
7+
*
8+
* Writes to THREE locations (all layers of OpenClaw's auth stack):
9+
* 1. openclaw.json — gateway-level provider/model config
10+
* 2. agents/main/agent/models.json — agent model catalog
11+
* 3. agents/main/agent/auth-profiles.json — agent API key store
12+
*
13+
* Supported env vars (set in .env):
14+
* 国内主流大模型 (Domestic AI providers):
15+
* DEEPSEEK_API_KEY → deepseek/deepseek-chat (custom provider)
16+
* BAIDU_API_KEY/SECRET → baidu/ernie-* (百度文心一言)
17+
* ALIBABA_API_KEY → alibaba/qwen-* (阿里通义千问)
18+
* MOONSHOT_API_KEY → moonshot/moonshot-v1-* (月之暗面 Kimi)
19+
*
20+
* WeCom channel (@sunnoy/wecom — community-enhanced plugin):
21+
* WebSocket persistent connection, no callback URL needed.
22+
* WECOM_BOT_ID → Bot ID (机器人ID) from WeCom AI Bot console
23+
* WECOM_BOT_SECRET → Bot Secret (机器人密钥) from WeCom AI Bot console
24+
* WECOM_DM_POLICY → Private chat policy: open|pairing|allowlist|disabled (default: open)
25+
* Optional Agent (enhanced outbound — file/image/dept/tag sending):
26+
* WECOM_AGENT_CORP_ID → corpId of self-built app (wwXXXXXX)
27+
* WECOM_AGENT_SECRET → corpSecret of self-built app
28+
* WECOM_AGENT_ID → agentId of self-built app (number)
29+
*/
30+
31+
'use strict';
32+
33+
const fs = require('fs');
34+
const path = require('path');
35+
36+
const OPENCLAW_DIR = '/home/node/.openclaw';
37+
const CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
38+
const AGENT_DIR = path.join(OPENCLAW_DIR, 'agents/main/agent');
39+
const AGENT_MODELS = path.join(AGENT_DIR, 'models.json');
40+
const AUTH_PROFILES = path.join(AGENT_DIR, 'auth-profiles.json');
41+
42+
fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
43+
fs.mkdirSync(AGENT_DIR, { recursive: true });
44+
45+
// ── Environment variable mapping (W9_*_SET UI-configurable vars) ─────────────
46+
// This allows Websoft9 app store to expose these as form fields while
47+
// maintaining backward compatibility with non-W9 variable names.
48+
const env = {
49+
// AI Models — W9_*_SET takes precedence, fallback to original name
50+
DEEPSEEK_API_KEY: process.env.W9_DEEPSEEK_API_KEY_SET || process.env.DEEPSEEK_API_KEY,
51+
BAIDU_API_KEY: process.env.W9_BAIDU_API_KEY_SET,
52+
BAIDU_SECRET_KEY: process.env.W9_BAIDU_SECRET_KEY_SET,
53+
ALIBABA_API_KEY: process.env.W9_ALIBABA_API_KEY_SET,
54+
MOONSHOT_API_KEY: process.env.W9_MOONSHOT_API_KEY_SET,
55+
56+
// WeCom (企业微信) — @sunnoy/wecom plugin
57+
WECOM_BOT_ID: process.env.W9_WECOM_BOT_ID_SET || process.env.WECOM_BOT_ID,
58+
WECOM_BOT_SECRET: process.env.W9_WECOM_BOT_SECRET_SET || process.env.WECOM_BOT_SECRET,
59+
WECOM_DM_POLICY: process.env.W9_WECOM_DM_POLICY_SET || process.env.WECOM_DM_POLICY || 'open',
60+
// Optional: Self-built App for enhanced outbound (file/image/dept/tag)
61+
WECOM_AGENT_CORP_ID: process.env.W9_WECOM_AGENT_CORP_ID_SET || process.env.WECOM_AGENT_CORP_ID,
62+
WECOM_AGENT_SECRET: process.env.W9_WECOM_AGENT_SECRET_SET || process.env.WECOM_AGENT_SECRET,
63+
WECOM_AGENT_ID: process.env.W9_WECOM_AGENT_ID_SET || process.env.WECOM_AGENT_ID,
64+
65+
};
66+
67+
// ── Load existing openclaw.json ───────────────────────────────────────────────
68+
let config = {};
69+
try {
70+
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
71+
console.log('[init] Loaded existing openclaw.json');
72+
} catch (_) {
73+
console.log('[init] Starting fresh openclaw.json');
74+
}
75+
76+
// ── Gateway (always overwrite from env) ───────────────────────────────────────
77+
config.gateway = {
78+
mode: 'local',
79+
bind: process.env.OPENCLAW_GATEWAY_BIND || 'lan',
80+
auth: { mode: 'token', token: process.env.OPENCLAW_GATEWAY_TOKEN },
81+
controlUi: {
82+
allowedOrigins: ['*'],
83+
// Disable per-device pairing for Docker deployments — the gateway token
84+
// already provides sufficient access control. Without this, every new
85+
// browser requires manual `openclaw devices approve` which is impractical.
86+
dangerouslyDisableDeviceAuth: true
87+
}
88+
};
89+
90+
// Only allow @sunnoy/wecom plugin (plugin id = "wecom", directory = "wecom")
91+
config.plugins = { allow: ['wecom'] };
92+
93+
// ── Build provider + auth lists ───────────────────────────────────────────────
94+
const gatewayProviders = (config.models && config.models.providers) || {};
95+
const agentModels = { providers: {} };
96+
const authProfiles = { version: 1, profiles: {} };
97+
98+
// ── DeepSeek (custom — needs config in all three files) ───────────────────────
99+
if (env.DEEPSEEK_API_KEY) {
100+
const deepseekDef = {
101+
baseUrl: 'https://api.deepseek.com/v1',
102+
apiKey: env.DEEPSEEK_API_KEY,
103+
api: 'openai-completions',
104+
authHeader: true,
105+
models: [
106+
{
107+
id: 'deepseek-chat', name: 'DeepSeek V3 (deepseek-chat)',
108+
api: 'openai-completions', reasoning: false, input: ['text'],
109+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
110+
contextWindow: 65536, maxTokens: 8192
111+
},
112+
{
113+
id: 'deepseek-reasoner', name: 'DeepSeek R1 (deepseek-reasoner)',
114+
api: 'openai-completions', reasoning: true, input: ['text'],
115+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
116+
contextWindow: 65536, maxTokens: 8192
117+
}
118+
]
119+
};
120+
gatewayProviders.deepseek = deepseekDef;
121+
agentModels.providers.deepseek = deepseekDef;
122+
authProfiles.profiles['deepseek:default'] = {
123+
type: 'api_key', mode: 'api_key',
124+
provider: 'deepseek',
125+
apiKey: env.DEEPSEEK_API_KEY
126+
};
127+
console.log('[init] DeepSeek configured (gateway + models + auth-profiles)');
128+
} else {
129+
delete gatewayProviders.deepseek;
130+
}
131+
132+
// ── Baidu ERNIE (百度文心) ────────────────────────────────────────────────────
133+
// Baidu Qianfan uses OAuth2: must exchange API Key + Secret Key for access_token
134+
// API doc: https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2
135+
if (env.BAIDU_API_KEY && env.BAIDU_SECRET_KEY) {
136+
const baiduDef = {
137+
baseUrl: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat',
138+
apiKey: env.BAIDU_API_KEY, // For OAuth token fetch
139+
secretKey: env.BAIDU_SECRET_KEY, // For OAuth token fetch
140+
api: 'baidu-chat', // Custom API adapter (OpenClaw will need to implement this)
141+
authHeader: false, // Auth via ?access_token= query param
142+
models: [
143+
{
144+
id: 'ernie-4.0-turbo-8k', name: 'ERNIE 4.0 Turbo',
145+
api: 'baidu-chat', reasoning: false, input: ['text'],
146+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
147+
contextWindow: 8192, maxTokens: 2048
148+
},
149+
{
150+
id: 'ernie-3.5-8k', name: 'ERNIE 3.5',
151+
api: 'baidu-chat', reasoning: false, input: ['text'],
152+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
153+
contextWindow: 8192, maxTokens: 2048
154+
},
155+
{
156+
id: 'ernie-speed-128k', name: 'ERNIE Speed 128K',
157+
api: 'baidu-chat', reasoning: false, input: ['text'],
158+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
159+
contextWindow: 131072, maxTokens: 4096
160+
}
161+
]
162+
};
163+
gatewayProviders.baidu = baiduDef;
164+
agentModels.providers.baidu = baiduDef;
165+
authProfiles.profiles['baidu:default'] = {
166+
type: 'api_key', mode: 'api_key',
167+
provider: 'baidu',
168+
apiKey: env.BAIDU_API_KEY,
169+
secretKey: env.BAIDU_SECRET_KEY // Non-standard: Baidu needs both keys
170+
};
171+
console.log('[init] Baidu ERNIE configured (requires baidu-chat API adapter)');
172+
} else {
173+
delete gatewayProviders.baidu;
174+
}
175+
176+
// ── Alibaba Qwen (阿里通义千问) ───────────────────────────────────────────────
177+
// DashScope API: https://help.aliyun.com/zh/dashscope/developer-reference/api-details
178+
if (env.ALIBABA_API_KEY) {
179+
const aliDef = {
180+
baseUrl: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
181+
apiKey: env.ALIBABA_API_KEY,
182+
api: 'qwen-chat', // Custom API adapter
183+
authHeader: true, // Authorization: Bearer <API-KEY>
184+
models: [
185+
{
186+
id: 'qwen-max', name: 'Qwen Max (最强推理)',
187+
api: 'qwen-chat', reasoning: false, input: ['text'],
188+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
189+
contextWindow: 30720, maxTokens: 8192
190+
},
191+
{
192+
id: 'qwen-plus', name: 'Qwen Plus (平衡性价比)',
193+
api: 'qwen-chat', reasoning: false, input: ['text'],
194+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
195+
contextWindow: 131072, maxTokens: 8192
196+
},
197+
{
198+
id: 'qwen-turbo', name: 'Qwen Turbo (极速响应)',
199+
api: 'qwen-chat', reasoning: false, input: ['text'],
200+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
201+
contextWindow: 131072, maxTokens: 8192
202+
}
203+
]
204+
};
205+
gatewayProviders.alibaba = aliDef;
206+
agentModels.providers.alibaba = aliDef;
207+
authProfiles.profiles['alibaba:default'] = {
208+
type: 'api_key', mode: 'api_key',
209+
provider: 'alibaba',
210+
apiKey: env.ALIBABA_API_KEY
211+
};
212+
console.log('[init] Alibaba Qwen configured (requires qwen-chat API adapter)');
213+
} else {
214+
delete gatewayProviders.alibaba;
215+
}
216+
217+
// ── Moonshot (月之暗面 Kimi) ──────────────────────────────────────────────────
218+
// OpenAI-compatible API: https://platform.moonshot.cn/docs/api-reference
219+
if (env.MOONSHOT_API_KEY) {
220+
const moonshotDef = {
221+
baseUrl: 'https://api.moonshot.cn/v1',
222+
apiKey: env.MOONSHOT_API_KEY,
223+
api: 'openai-completions', // OpenAI-compatible
224+
authHeader: true,
225+
models: [
226+
{
227+
id: 'moonshot-v1-8k', name: 'Moonshot v1 8K',
228+
api: 'openai-completions', reasoning: false, input: ['text'],
229+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
230+
contextWindow: 8192, maxTokens: 4096
231+
},
232+
{
233+
id: 'moonshot-v1-32k', name: 'Moonshot v1 32K',
234+
api: 'openai-completions', reasoning: false, input: ['text'],
235+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
236+
contextWindow: 32768, maxTokens: 4096
237+
},
238+
{
239+
id: 'moonshot-v1-128k', name: 'Moonshot v1 128K',
240+
api: 'openai-completions', reasoning: false, input: ['text'],
241+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
242+
contextWindow: 131072, maxTokens: 4096
243+
}
244+
]
245+
};
246+
gatewayProviders.moonshot = moonshotDef;
247+
agentModels.providers.moonshot = moonshotDef;
248+
authProfiles.profiles['moonshot:default'] = {
249+
type: 'api_key', mode: 'api_key',
250+
provider: 'moonshot',
251+
apiKey: env.MOONSHOT_API_KEY
252+
};
253+
console.log('[init] Moonshot configured (OpenAI-compatible)');
254+
} else {
255+
delete gatewayProviders.moonshot;
256+
}
257+
258+
// ── WeCom Channel (@sunnoy/wecom) ────────────────────────────────────────────
259+
// WebSocket persistent connection — no callback URL needed.
260+
// Plugin is guaranteed by Dockerfile, no filesystem check required.
261+
// Optional Agent config enables file/image/dept/tag outbound sending.
262+
const hasWeCom = !!(env.WECOM_BOT_ID && env.WECOM_BOT_SECRET);
263+
const hasWeComAgent = !!(env.WECOM_AGENT_CORP_ID && env.WECOM_AGENT_SECRET && env.WECOM_AGENT_ID);
264+
265+
if (hasWeCom) {
266+
const wecomConfig = {
267+
enabled: true,
268+
botId: env.WECOM_BOT_ID,
269+
secret: env.WECOM_BOT_SECRET,
270+
// dmPolicy: open = all members can chat directly (no pairing approval needed)
271+
// options: open | pairing | allowlist | disabled
272+
dmPolicy: env.WECOM_DM_POLICY
273+
};
274+
275+
// Optional: Self-built App for enhanced outbound (file/image/dept/tag sending)
276+
// Does NOT need token/encodingAESKey — Agent only handles outbound, not inbound.
277+
if (hasWeComAgent) {
278+
wecomConfig.agent = {
279+
corpId: env.WECOM_AGENT_CORP_ID,
280+
corpSecret: env.WECOM_AGENT_SECRET,
281+
agentId: parseInt(env.WECOM_AGENT_ID, 10)
282+
};
283+
}
284+
285+
config.channels = config.channels || {};
286+
config.channels['wecom'] = wecomConfig;
287+
288+
// Add/update binding to the main agent (preserving any other bindings)
289+
const wecomBinding = { agentId: 'main', match: { channel: 'wecom' } };
290+
const otherBindings = (Array.isArray(config.bindings) ? config.bindings : [])
291+
.filter(b => !(b.match && b.match.channel === 'wecom'));
292+
config.bindings = [...otherBindings, wecomBinding];
293+
294+
const agentLabel = hasWeComAgent ? ' + Agent(outbound)' : '';
295+
console.log('[init] WeCom configured (WebSocket' + agentLabel + ', dmPolicy=' + env.WECOM_DM_POLICY + ', botId=' + env.WECOM_BOT_ID + ')');
296+
} else {
297+
// Remove WeCom config when credentials not provided
298+
if (config.channels && config.channels['wecom']) {
299+
delete config.channels['wecom'];
300+
if (Object.keys(config.channels).length === 0) delete config.channels;
301+
}
302+
if (Array.isArray(config.bindings)) {
303+
config.bindings = config.bindings.filter(
304+
b => !(b.match && b.match.channel === 'wecom')
305+
);
306+
if (config.bindings.length === 0) delete config.bindings;
307+
}
308+
console.log('[init] WeCom skipped — set W9_WECOM_BOT_ID_SET and W9_WECOM_BOT_SECRET_SET to enable');
309+
}
310+
311+
// ── Write models.json ─────────────────────────────────────────────────────────
312+
if (Object.keys(agentModels.providers).length > 0) {
313+
fs.writeFileSync(AGENT_MODELS, JSON.stringify(agentModels, null, 2));
314+
console.log('[init] models.json written');
315+
} else {
316+
try { fs.unlinkSync(AGENT_MODELS); } catch (_) { }
317+
}
318+
319+
// ── Write auth-profiles.json ──────────────────────────────────────────────────
320+
if (Object.keys(authProfiles.profiles).length > 0) {
321+
fs.writeFileSync(AUTH_PROFILES, JSON.stringify(authProfiles, null, 2));
322+
console.log('[init] auth-profiles.json written');
323+
} else {
324+
try { fs.unlinkSync(AUTH_PROFILES); } catch (_) { }
325+
}
326+
327+
// ── Write openclaw.json (gateway + model defaults) ────────────────────────────
328+
if (Object.keys(gatewayProviders).length > 0) {
329+
config.models = { mode: 'merge', providers: gatewayProviders };
330+
} else {
331+
delete config.models;
332+
}
333+
334+
// Default model — always recalculate so stale refs are cleared if key removed
335+
// Priority: DeepSeek → Moonshot → Baidu → Alibaba
336+
let defaultModel = null;
337+
if (env.DEEPSEEK_API_KEY) defaultModel = 'deepseek/deepseek-chat';
338+
else if (env.MOONSHOT_API_KEY) defaultModel = 'moonshot/moonshot-v1-32k';
339+
else if (env.BAIDU_API_KEY) defaultModel = 'baidu/ernie-4.0-turbo-8k';
340+
else if (env.ALIBABA_API_KEY) defaultModel = 'alibaba/qwen-max';
341+
342+
if (defaultModel) {
343+
config.agents = config.agents || {};
344+
config.agents.defaults = config.agents.defaults || {};
345+
config.agents.defaults.model = { primary: defaultModel };
346+
config.agents.defaults.workspace = config.agents.defaults.workspace || '~/.openclaw/workspace';
347+
console.log('[init] Default model:', defaultModel);
348+
} else {
349+
if (config.agents && config.agents.defaults) delete config.agents.defaults.model;
350+
console.log('[init] No API key — configure one in .env and restart');
351+
}
352+
353+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
354+
console.log('[init] openclaw.json written');

0 commit comments

Comments
 (0)