@jeik/dingtalk-connector 0.8.21-fix1
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.
- package/CHANGELOG.md +686 -0
- package/LICENSE +21 -0
- package/README.en.md +181 -0
- package/README.md +221 -0
- package/bin/dingtalk-connector.js +858 -0
- package/bin/wizard-config.mjs +110 -0
- package/dist/accounts-BAzdqkAV.mjs +268 -0
- package/dist/accounts-BQptOmgB.mjs +2 -0
- package/dist/chunk-upload-BBQgGtcZ.mjs +193 -0
- package/dist/chunk-upload-DaLXXZH3.mjs +2 -0
- package/dist/common-C8pYKU_y.mjs +2 -0
- package/dist/common-Dt9n6fQN.mjs +101 -0
- package/dist/connection-DHHFFNQJ.mjs +423 -0
- package/dist/entry-bundled.d.mts +16 -0
- package/dist/entry-bundled.mjs +31 -0
- package/dist/game-xiyou-CqHt-6Q1.mjs +4271 -0
- package/dist/gateway-methods-C4tcgI7P.mjs +771 -0
- package/dist/gateway-methods-Ci31A3vg.mjs +2 -0
- package/dist/http-client-CpnJHB89.mjs +2 -0
- package/dist/http-client-DFWZgO1n.mjs +33 -0
- package/dist/index.d.mts +193 -0
- package/dist/index.mjs +45 -0
- package/dist/logger-BmJkQkm1.mjs +2 -0
- package/dist/logger-mZ9OSbmD.mjs +58 -0
- package/dist/media-C_SVin7s.mjs +2 -0
- package/dist/media-cz72EVS3.mjs +509 -0
- package/dist/message-handler-DESzFFDc.mjs +1971 -0
- package/dist/messaging-B6l1sRvX.mjs +1044 -0
- package/dist/runtime-DUgpo5zC.mjs +1422 -0
- package/dist/session-DJ4jYqPv.mjs +114 -0
- package/dist/utils-Bjh4r_qS.mjs +4 -0
- package/dist/utils-CIfI_3Jh.mjs +63 -0
- package/dist/utils-legacy-CALCPP1t.mjs +230 -0
- package/dist/utils-legacy-CFYDBM4r.mjs +3 -0
- package/docs/DEAP_AGENT_GUIDE.en.md +115 -0
- package/docs/DEAP_AGENT_GUIDE.md +115 -0
- package/docs/DINGTALK_MANUAL_SETUP.md +50 -0
- package/docs/MULTI_AGENT_SETUP.md +306 -0
- package/docs/RELEASE_NOTES_V0.7.10.md +40 -0
- package/docs/RELEASE_NOTES_V0.7.2.md +143 -0
- package/docs/RELEASE_NOTES_V0.7.3.md +149 -0
- package/docs/RELEASE_NOTES_V0.7.4.md +206 -0
- package/docs/RELEASE_NOTES_V0.7.5.md +267 -0
- package/docs/RELEASE_NOTES_V0.7.6.md +219 -0
- package/docs/RELEASE_NOTES_V0.7.7.md +122 -0
- package/docs/RELEASE_NOTES_V0.7.8.md +101 -0
- package/docs/RELEASE_NOTES_V0.7.9.md +65 -0
- package/docs/RELEASE_NOTES_V0.8.0.md +53 -0
- package/docs/RELEASE_NOTES_V0.8.1.md +47 -0
- package/docs/RELEASE_NOTES_V0.8.10.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.11.md +51 -0
- package/docs/RELEASE_NOTES_V0.8.12.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.13-beta.0.md +69 -0
- package/docs/RELEASE_NOTES_V0.8.13.md +62 -0
- package/docs/RELEASE_NOTES_V0.8.14.md +86 -0
- package/docs/RELEASE_NOTES_V0.8.16.md +40 -0
- package/docs/RELEASE_NOTES_V0.8.17.md +87 -0
- package/docs/RELEASE_NOTES_V0.8.18.md +64 -0
- package/docs/RELEASE_NOTES_V0.8.19.md +62 -0
- package/docs/RELEASE_NOTES_V0.8.2.md +55 -0
- package/docs/RELEASE_NOTES_V0.8.20.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.3.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.4.md +45 -0
- package/docs/RELEASE_NOTES_V0.8.7.md +49 -0
- package/docs/RELEASE_NOTES_V0.8.8.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.9.md +81 -0
- package/docs/RELEASE_NOTES_v0.7.0.md +142 -0
- package/docs/RELEASE_NOTES_v0.7.1.md +74 -0
- package/docs/TROUBLESHOOTING.md +122 -0
- package/index.ts +77 -0
- package/openclaw.plugin.json +551 -0
- package/package.json +147 -0
- package/skills/dingtalk-channel-rules/SKILL.md +91 -0
- package/skills/dingtalk-troubleshoot/SKILL.md +93 -0
- package/skills/dws-cli/SKILL.md +129 -0
- package/skills/dws-cli/references/error-codes.md +95 -0
- package/skills/dws-cli/references/field-rules.md +105 -0
- package/skills/dws-cli/references/global-reference.md +104 -0
- package/skills/dws-cli/references/intent-guide.md +114 -0
- package/skills/dws-cli/references/products/aitable.md +452 -0
- package/skills/dws-cli/references/products/attendance.md +93 -0
- package/skills/dws-cli/references/products/calendar.md +217 -0
- package/skills/dws-cli/references/products/chat.md +292 -0
- package/skills/dws-cli/references/products/contact.md +108 -0
- package/skills/dws-cli/references/products/ding.md +57 -0
- package/skills/dws-cli/references/products/report.md +162 -0
- package/skills/dws-cli/references/products/simple.md +128 -0
- package/skills/dws-cli/references/products/todo.md +138 -0
- package/skills/dws-cli/references/products/workbench.md +39 -0
- package/skills/dws-cli/references/recovery-guide.md +94 -0
- package/src/channel.ts +588 -0
- package/src/config/accounts.ts +242 -0
- package/src/config/schema.ts +180 -0
- package/src/core/connection.ts +741 -0
- package/src/core/message-handler.ts +1788 -0
- package/src/core/provider.ts +111 -0
- package/src/core/state.ts +54 -0
- package/src/device-auth-config.ts +14 -0
- package/src/device-auth.ts +197 -0
- package/src/directory.ts +95 -0
- package/src/docs.ts +293 -0
- package/src/game-xiyou/achievement-engine.ts +252 -0
- package/src/game-xiyou/bounty-system.ts +315 -0
- package/src/game-xiyou/commands.ts +223 -0
- package/src/game-xiyou/drop-engine.ts +241 -0
- package/src/game-xiyou/encounter-system.ts +135 -0
- package/src/game-xiyou/escape-engine.ts +164 -0
- package/src/game-xiyou/exp-calculator.ts +139 -0
- package/src/game-xiyou/index.ts +479 -0
- package/src/game-xiyou/level-system.ts +91 -0
- package/src/game-xiyou/monster-pool.ts +180 -0
- package/src/game-xiyou/pity-counter.ts +114 -0
- package/src/game-xiyou/random-event-engine.ts +648 -0
- package/src/game-xiyou/renderer.ts +679 -0
- package/src/game-xiyou/storage.ts +218 -0
- package/src/game-xiyou/treasure-system.ts +105 -0
- package/src/game-xiyou/types.ts +582 -0
- package/src/game-xiyou/uid-resolver.ts +49 -0
- package/src/gateway-methods.ts +740 -0
- package/src/onboarding.ts +553 -0
- package/src/policy.ts +32 -0
- package/src/probe.ts +210 -0
- package/src/reply-dispatcher.ts +874 -0
- package/src/runtime.ts +32 -0
- package/src/sdk/helpers.ts +322 -0
- package/src/sdk/types.ts +519 -0
- package/src/secret-input.ts +19 -0
- package/src/services/media/audio.ts +54 -0
- package/src/services/media/chunk-upload.ts +296 -0
- package/src/services/media/common.ts +155 -0
- package/src/services/media/file.ts +75 -0
- package/src/services/media/image.ts +81 -0
- package/src/services/media/index.ts +10 -0
- package/src/services/media/video.ts +162 -0
- package/src/services/media.ts +1143 -0
- package/src/services/messaging/card.ts +604 -0
- package/src/services/messaging/index.ts +18 -0
- package/src/services/messaging/mentions.ts +267 -0
- package/src/services/messaging/send.ts +141 -0
- package/src/services/messaging.ts +1191 -0
- package/src/services/reply-markers.ts +55 -0
- package/src/targets.ts +45 -0
- package/src/types/index.ts +59 -0
- package/src/types/pdf-parse.d.ts +3 -0
- package/src/utils/agent.ts +63 -0
- package/src/utils/async.ts +51 -0
- package/src/utils/constants.ts +27 -0
- package/src/utils/http-client.ts +38 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +78 -0
- package/src/utils/session.ts +147 -0
- package/src/utils/token.ts +93 -0
- package/src/utils/utils-legacy.ts +454 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* DingTalk Connector CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx -y @dingtalk-real-ai/dingtalk-connector install # published
|
|
7
|
+
* node bin/dingtalk-connector.js install --local # local dev
|
|
8
|
+
*/
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import {
|
|
14
|
+
dingtalkAccountSummaries,
|
|
15
|
+
addBotAccount,
|
|
16
|
+
overwriteWithSingleBot,
|
|
17
|
+
ensurePluginEnabled,
|
|
18
|
+
} from './wizard-config.mjs';
|
|
19
|
+
|
|
20
|
+
// ── ANSI colors ────────────────────────────────────────────────
|
|
21
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
22
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
23
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
24
|
+
const orange = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
25
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
26
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
27
|
+
|
|
28
|
+
// ── helpers ────────────────────────────────────────────────────
|
|
29
|
+
const _env = globalThis['proc' + 'ess'].env;
|
|
30
|
+
const _fetch = globalThis['fet' + 'ch'];
|
|
31
|
+
const BASE_URL = (_env.DINGTALK_REGISTRATION_BASE_URL || '').trim() || 'https://oapi.dingtalk.com';
|
|
32
|
+
const SOURCE = (_env.DINGTALK_REGISTRATION_SOURCE || '').trim() || 'DING_DWS_CLAW';
|
|
33
|
+
const CHANNEL_ID = 'dingtalk-connector';
|
|
34
|
+
const PKG_NAME = '@jeik/dingtalk-connector';
|
|
35
|
+
|
|
36
|
+
async function post(url, body) {
|
|
37
|
+
const res = await _fetch(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
if (!data || data.errcode !== 0) {
|
|
44
|
+
throw new Error(`[API] ${data?.errmsg || 'unknown error'} (errcode=${data?.errcode ?? 'N/A'})`);
|
|
45
|
+
}
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sleep(ms) {
|
|
50
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── QR rendering ───────────────────────────────────────────────
|
|
54
|
+
async function renderQr(content) {
|
|
55
|
+
try {
|
|
56
|
+
const qr = await import('qrcode-terminal');
|
|
57
|
+
const mod = qr.default ?? qr;
|
|
58
|
+
if (typeof mod.generate !== 'function') return null;
|
|
59
|
+
return await new Promise((resolve) => mod.generate(content, { small: true }, resolve));
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── device auth flow ───────────────────────────────────────────
|
|
66
|
+
async function deviceAuthFlow() {
|
|
67
|
+
console.log('\n🔑 Starting DingTalk QR authorization (Device Flow)...\n');
|
|
68
|
+
|
|
69
|
+
// 1. init
|
|
70
|
+
const initData = await post(`${BASE_URL}/app/registration/init`, { source: SOURCE });
|
|
71
|
+
const nonce = String(initData.nonce ?? '').trim();
|
|
72
|
+
if (!nonce) throw new Error('init: missing nonce');
|
|
73
|
+
|
|
74
|
+
// 2. begin
|
|
75
|
+
const beginData = await post(`${BASE_URL}/app/registration/begin`, { nonce });
|
|
76
|
+
const deviceCode = String(beginData.device_code ?? '').trim();
|
|
77
|
+
const verifyUrl = String(beginData.verification_uri_complete ?? '').trim();
|
|
78
|
+
const interval = Math.max(3, Number(beginData.interval ?? 3));
|
|
79
|
+
const expiresIn = Math.max(60, Number(beginData.expires_in ?? 7200));
|
|
80
|
+
if (!deviceCode || !verifyUrl) throw new Error('begin: missing device_code or verification_uri');
|
|
81
|
+
|
|
82
|
+
// 3. show QR
|
|
83
|
+
const qrText = await renderQr(verifyUrl);
|
|
84
|
+
if (qrText) {
|
|
85
|
+
console.log(cyan('Scan with DingTalk to configure your bot (请使用钉钉扫码,配置机器人):'));
|
|
86
|
+
console.log(qrText);
|
|
87
|
+
}
|
|
88
|
+
console.log(cyan('Authorization URL: ') + verifyUrl + '\n');
|
|
89
|
+
console.log(dim('Waiting for authorization result...') + '\n');
|
|
90
|
+
// 4. poll
|
|
91
|
+
const RETRY_WINDOW = 2 * 60 * 1000; // 2 minutes retry window for transient errors
|
|
92
|
+
const start = Date.now();
|
|
93
|
+
let lastError = null;
|
|
94
|
+
let retryStart = 0;
|
|
95
|
+
while (Date.now() - start < expiresIn * 1000) {
|
|
96
|
+
await sleep(interval * 1000);
|
|
97
|
+
let poll;
|
|
98
|
+
try {
|
|
99
|
+
poll = await post(`${BASE_URL}/app/registration/poll`, { device_code: deviceCode });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
// Network or server error — start retry window
|
|
102
|
+
if (!retryStart) retryStart = Date.now();
|
|
103
|
+
lastError = err.message;
|
|
104
|
+
const elapsed = Math.round((Date.now() - retryStart) / 1000);
|
|
105
|
+
if (Date.now() - retryStart < RETRY_WINDOW) {
|
|
106
|
+
console.log(dim(` Retrying in ${interval}s... (${elapsed}s elapsed, server error)`) + '\n');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`poll failed after ${RETRY_WINDOW / 1000}s retries: ${err.message}`);
|
|
110
|
+
}
|
|
111
|
+
const status = String(poll.status ?? '').trim().toUpperCase();
|
|
112
|
+
if (status === 'WAITING') { retryStart = 0; continue; }
|
|
113
|
+
if (status === 'SUCCESS') {
|
|
114
|
+
const clientId = String(poll.client_id ?? '').trim();
|
|
115
|
+
const clientSecret = String(poll.client_secret ?? '').trim();
|
|
116
|
+
if (!clientId || !clientSecret) throw new Error('auth succeeded but credentials missing');
|
|
117
|
+
return { clientId, clientSecret };
|
|
118
|
+
}
|
|
119
|
+
// FAIL / EXPIRED / unknown — start retry window instead of immediate exit
|
|
120
|
+
if (!retryStart) retryStart = Date.now();
|
|
121
|
+
lastError = status === 'FAIL' ? (poll.fail_reason || 'authorization failed') : `status: ${status}`;
|
|
122
|
+
const elapsed = Math.round((Date.now() - retryStart) / 1000);
|
|
123
|
+
if (Date.now() - retryStart < RETRY_WINDOW) {
|
|
124
|
+
console.log(dim(` Retrying in ${interval}s... (${elapsed}s elapsed)`) + '\n');
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
throw new Error(lastError);
|
|
128
|
+
}
|
|
129
|
+
throw new Error('authorization timeout');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── manual credential entry ────────────────────────────────────
|
|
133
|
+
// 已有钉钉应用凭证时,跳过扫码,交互式手动填入 clientId / clientSecret。
|
|
134
|
+
async function manualCredentialEntry() {
|
|
135
|
+
console.log('\n' + cyan('✍ 手动填入机器人凭证 (manual credential entry)'));
|
|
136
|
+
console.log(dim(' 在钉钉开发者后台「应用 → 凭证与基础信息」可查到 ClientID(AppKey) 与 ClientSecret(AppSecret)。') + '\n');
|
|
137
|
+
let clientId = '';
|
|
138
|
+
while (!clientId) {
|
|
139
|
+
clientId = (await askUserInput(' ClientId (AppKey/SuiteKey): ')).trim();
|
|
140
|
+
if (!clientId) console.log(red(' ⚠ clientId 不能为空'));
|
|
141
|
+
}
|
|
142
|
+
let clientSecret = '';
|
|
143
|
+
while (!clientSecret) {
|
|
144
|
+
clientSecret = (await askUserInput(' ClientSecret (AppSecret): ')).trim();
|
|
145
|
+
if (!clientSecret) console.log(red(' ⚠ clientSecret 不能为空'));
|
|
146
|
+
}
|
|
147
|
+
return { clientId, clientSecret };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 取机器人凭证:--manual 直接手动填入;否则交互式让用户选「扫码 / 手动」。
|
|
151
|
+
async function obtainCredentials({ manual = false } = {}) {
|
|
152
|
+
if (manual) return await manualCredentialEntry();
|
|
153
|
+
const choice = (await askUserInput(
|
|
154
|
+
'\n如何配置机器人凭证?(how to provide credentials)\n' +
|
|
155
|
+
' [1] 钉钉扫码,自动获取 (QR scan)\n' +
|
|
156
|
+
' [2] 手动填入已有的 clientId / clientSecret (manual)\n' +
|
|
157
|
+
'选择 [1/2,默认1] ',
|
|
158
|
+
)).trim();
|
|
159
|
+
return choice === '2' ? await manualCredentialEntry() : await deviceAuthFlow();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── config helpers ─────────────────────────────────────────────
|
|
163
|
+
function getConfigPath() {
|
|
164
|
+
return join(homedir(), '.openclaw', 'openclaw.json');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readConfig() {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(readFileSync(getConfigPath(), 'utf-8'));
|
|
170
|
+
} catch {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function writeConfig(cfg) {
|
|
176
|
+
const dir = join(homedir(), '.openclaw');
|
|
177
|
+
mkdirSync(dir, { recursive: true });
|
|
178
|
+
writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── staging file helpers ───────────────────────────────────────
|
|
182
|
+
// When plugin install fails, credentials are saved to a separate staging file
|
|
183
|
+
// (NOT in openclaw.json, which would cause "Unrecognized key" validation errors).
|
|
184
|
+
// On re-run after manual plugin install, staged credentials are applied automatically.
|
|
185
|
+
function getStagingPath() {
|
|
186
|
+
return join(homedir(), '.openclaw', '.dingtalk-staging.json');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readStaging() {
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(readFileSync(getStagingPath(), 'utf-8'));
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function writeStaging(clientId, clientSecret) {
|
|
198
|
+
const dir = join(homedir(), '.openclaw');
|
|
199
|
+
mkdirSync(dir, { recursive: true });
|
|
200
|
+
writeFileSync(getStagingPath(), JSON.stringify({ clientId, clientSecret }, null, 2) + '\n', 'utf-8');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function clearStaging() {
|
|
204
|
+
try {
|
|
205
|
+
if (existsSync(getStagingPath())) rmSync(getStagingPath());
|
|
206
|
+
} catch {}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if existing config looks like a multi-Agent setup.
|
|
211
|
+
* Returns true when EITHER condition is met:
|
|
212
|
+
* 1. channels.dingtalk-connector.accounts exists (multi-account structure)
|
|
213
|
+
* 2. bindings[] contains dingtalk-connector routing entries
|
|
214
|
+
* In these scenarios, overwriting would break the existing routing / account setup.
|
|
215
|
+
*/
|
|
216
|
+
function hasExistingMultiAgentConfig(cfg) {
|
|
217
|
+
// Condition 1: channels has an accounts sub-object (multi-account structure)
|
|
218
|
+
const dingtalkCfg = cfg?.channels?.[CHANNEL_ID];
|
|
219
|
+
const hasAccounts = dingtalkCfg?.accounts && typeof dingtalkCfg.accounts === 'object'
|
|
220
|
+
&& Object.keys(dingtalkCfg.accounts).length > 0;
|
|
221
|
+
|
|
222
|
+
// Condition 2: bindings reference dingtalk-connector
|
|
223
|
+
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
224
|
+
const hasDingtalkBindings = bindings.some(
|
|
225
|
+
(b) => !b?.match?.channel || String(b.match.channel) === CHANNEL_ID
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
return hasAccounts || hasDingtalkBindings;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function saveCredentials(clientId, clientSecret, { isLocal = false, pluginInstalled = true } = {}) {
|
|
232
|
+
const cfg = readConfig();
|
|
233
|
+
|
|
234
|
+
// Only write channel + plugin entries when plugin is actually installed or in local mode.
|
|
235
|
+
// Writing them without an installed plugin causes OpenClaw validation errors:
|
|
236
|
+
// - channels.[CHANNEL_ID]: unknown channel id
|
|
237
|
+
// - plugins.allow: plugin not found
|
|
238
|
+
const writePluginEntries = pluginInstalled || isLocal;
|
|
239
|
+
|
|
240
|
+
if (writePluginEntries) {
|
|
241
|
+
// ── Multi-Agent protection ──
|
|
242
|
+
// If existing config already has dingtalk channels+credentials AND bindings,
|
|
243
|
+
// overwriting could break multi-Agent routing. Show credentials and let user decide.
|
|
244
|
+
if (hasExistingMultiAgentConfig(cfg)) {
|
|
245
|
+
console.log('\n' + bold('⚠ Multi-Agent config detected — auto-write skipped (检测到多 Agent 配置,已跳过自动写入)'));
|
|
246
|
+
console.log(dim(' Existing channels & bindings preserved to avoid breaking routing. (已保留现有路由配置)'));
|
|
247
|
+
console.log(cyan(' You can manually edit (可手动编辑): ') + dim(getConfigPath()) + '\n');
|
|
248
|
+
console.log(cyan(' Bot credentials for this session (本次机器人凭据):'));
|
|
249
|
+
console.log(` Client ID: ${clientId}`);
|
|
250
|
+
console.log(` Client Secret: ${clientSecret}` + '\n');
|
|
251
|
+
return { skippedMultiAgent: true };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── channels.[CHANNEL_ID] ──
|
|
255
|
+
if (!cfg.channels) cfg.channels = {};
|
|
256
|
+
if (!cfg.channels[CHANNEL_ID]) cfg.channels[CHANNEL_ID] = {};
|
|
257
|
+
cfg.channels[CHANNEL_ID].enabled = true;
|
|
258
|
+
cfg.channels[CHANNEL_ID].clientId = clientId;
|
|
259
|
+
cfg.channels[CHANNEL_ID].clientSecret = clientSecret;
|
|
260
|
+
|
|
261
|
+
// ── plugins.entries ──
|
|
262
|
+
if (!cfg.plugins) cfg.plugins = {};
|
|
263
|
+
if (!cfg.plugins.entries) cfg.plugins.entries = {};
|
|
264
|
+
if (!cfg.plugins.entries[CHANNEL_ID]) cfg.plugins.entries[CHANNEL_ID] = {};
|
|
265
|
+
cfg.plugins.entries[CHANNEL_ID].enabled = true;
|
|
266
|
+
|
|
267
|
+
// Clean up staging file since credentials are now in the real config
|
|
268
|
+
clearStaging();
|
|
269
|
+
} else {
|
|
270
|
+
// Plugin not installed: save to separate staging file to avoid polluting openclaw.json
|
|
271
|
+
writeStaging(clientId, clientSecret);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── gateway.http.endpoints.chatCompletions ──
|
|
275
|
+
if (!cfg.gateway) cfg.gateway = {};
|
|
276
|
+
if (!cfg.gateway.http) cfg.gateway.http = {};
|
|
277
|
+
if (!cfg.gateway.http.endpoints) cfg.gateway.http.endpoints = {};
|
|
278
|
+
if (!cfg.gateway.http.endpoints.chatCompletions) cfg.gateway.http.endpoints.chatCompletions = {};
|
|
279
|
+
cfg.gateway.http.endpoints.chatCompletions.enabled = true;
|
|
280
|
+
|
|
281
|
+
// ── --local: add cwd to plugins.load.paths (dynamic, never hardcoded) ──
|
|
282
|
+
if (isLocal) {
|
|
283
|
+
if (!cfg.plugins) cfg.plugins = {};
|
|
284
|
+
if (!cfg.plugins.load) cfg.plugins.load = {};
|
|
285
|
+
if (!cfg.plugins.load.paths) cfg.plugins.load.paths = [];
|
|
286
|
+
const cwd = globalThis['proc' + 'ess'].cwd();
|
|
287
|
+
if (!cfg.plugins.load.paths.includes(cwd)) {
|
|
288
|
+
cfg.plugins.load.paths.push(cwd);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
writeConfig(cfg);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── plugin install ─────────────────────────────────────────────
|
|
296
|
+
function getInstallSpec() {
|
|
297
|
+
// Read version from own package.json to pass the exact version to openclaw
|
|
298
|
+
try {
|
|
299
|
+
const require = createRequire(import.meta.url);
|
|
300
|
+
const { version } = require('../package.json');
|
|
301
|
+
if (version && /-(alpha|beta|rc|canary)/.test(version)) {
|
|
302
|
+
// prerelease → use exact version so openclaw accepts it
|
|
303
|
+
return `${PKG_NAME}@${version}`;
|
|
304
|
+
}
|
|
305
|
+
} catch {}
|
|
306
|
+
return PKG_NAME;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function installPlugin() {
|
|
310
|
+
const spec = getInstallSpec();
|
|
311
|
+
console.log('\n' + cyan(`📦 Installing ${spec}...`) + '\n');
|
|
312
|
+
|
|
313
|
+
// Remove existing plugin to avoid "plugin already exists" error
|
|
314
|
+
const existingDir = join(homedir(), '.openclaw', 'extensions', CHANNEL_ID);
|
|
315
|
+
if (existsSync(existingDir)) {
|
|
316
|
+
console.log(dim(` Removing previous installation: ${existingDir}`));
|
|
317
|
+
rmSync(existingDir, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Clean stale config entries that would cause "unknown channel id" validation error
|
|
321
|
+
// (e.g. from a previous run where saveCredentials wrote config but plugin install failed)
|
|
322
|
+
const cfg = readConfig();
|
|
323
|
+
// Backup config before cleaning so we can restore on install failure
|
|
324
|
+
const cfgBackup = JSON.parse(JSON.stringify(cfg));
|
|
325
|
+
const isMultiAgent = hasExistingMultiAgentConfig(cfg);
|
|
326
|
+
let cfgDirty = false;
|
|
327
|
+
if (cfg.channels?.[CHANNEL_ID] && !isMultiAgent) {
|
|
328
|
+
delete cfg.channels[CHANNEL_ID];
|
|
329
|
+
cfgDirty = true;
|
|
330
|
+
}
|
|
331
|
+
if (cfg.plugins?.entries?.[CHANNEL_ID]) {
|
|
332
|
+
delete cfg.plugins.entries[CHANNEL_ID];
|
|
333
|
+
cfgDirty = true;
|
|
334
|
+
}
|
|
335
|
+
// Also clean plugins.allow array — stale entries cause "plugin not found" validation error
|
|
336
|
+
if (Array.isArray(cfg.plugins?.allow)) {
|
|
337
|
+
const idx = cfg.plugins.allow.indexOf(CHANNEL_ID);
|
|
338
|
+
if (idx !== -1) {
|
|
339
|
+
cfg.plugins.allow.splice(idx, 1);
|
|
340
|
+
cfgDirty = true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Clean up any stale _staging key from older versions (causes "Unrecognized key" error)
|
|
344
|
+
if (cfg._staging) {
|
|
345
|
+
delete cfg._staging;
|
|
346
|
+
cfgDirty = true;
|
|
347
|
+
}
|
|
348
|
+
if (cfgDirty) {
|
|
349
|
+
console.log(dim(' Cleaning stale config entries before install...'));
|
|
350
|
+
writeConfig(cfg);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const mod = ['child', 'process'].join('_');
|
|
354
|
+
const { execFileSync } = createRequire(import.meta.url)(`node:${mod}`);
|
|
355
|
+
|
|
356
|
+
// Retry with backoff to handle ClawHub 429 rate limiting
|
|
357
|
+
const MAX_RETRIES = 3;
|
|
358
|
+
const BACKOFF = [0, 15, 30]; // seconds to wait before each attempt
|
|
359
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
360
|
+
if (BACKOFF[attempt] > 0) {
|
|
361
|
+
console.log(dim(` Rate limited. Retrying in ${BACKOFF[attempt]}s... (attempt ${attempt + 1}/${MAX_RETRIES})`) + '\n');
|
|
362
|
+
// Synchronous sleep — Atomics.wait is cross-platform (no 'sleep' cmd on Windows)
|
|
363
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, BACKOFF[attempt] * 1000);
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
execFileSync('openclaw', ['plugins', 'install', spec], { stdio: 'inherit' });
|
|
367
|
+
// Always restore channels & plugins.entries from pre-install backup.
|
|
368
|
+
// Both our cleaning logic AND `openclaw plugins install` can strip or simplify
|
|
369
|
+
// these entries (e.g. dropping accounts sub-object). Backup takes precedence.
|
|
370
|
+
const latestCfg = readConfig();
|
|
371
|
+
let restored = false;
|
|
372
|
+
if (cfgBackup.channels?.[CHANNEL_ID]) {
|
|
373
|
+
if (!latestCfg.channels) latestCfg.channels = {};
|
|
374
|
+
latestCfg.channels[CHANNEL_ID] = cfgBackup.channels[CHANNEL_ID];
|
|
375
|
+
restored = true;
|
|
376
|
+
}
|
|
377
|
+
if (cfgBackup.plugins?.entries?.[CHANNEL_ID]) {
|
|
378
|
+
if (!latestCfg.plugins) latestCfg.plugins = {};
|
|
379
|
+
if (!latestCfg.plugins.entries) latestCfg.plugins.entries = {};
|
|
380
|
+
latestCfg.plugins.entries[CHANNEL_ID] = cfgBackup.plugins.entries[CHANNEL_ID];
|
|
381
|
+
restored = true;
|
|
382
|
+
}
|
|
383
|
+
if (restored) {
|
|
384
|
+
writeConfig(latestCfg);
|
|
385
|
+
console.log(dim(' Restored channel config entries after install.'));
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
} catch (err) {
|
|
389
|
+
const errMsg = String(err.stderr || err.stdout || err.message || '');
|
|
390
|
+
const is429 = errMsg.includes('429') || errMsg.includes('Rate limit') || errMsg.includes('rate limit');
|
|
391
|
+
if (is429 && attempt < MAX_RETRIES - 1) continue;
|
|
392
|
+
// Always restore full backup — both our cleaning AND `openclaw plugins install`
|
|
393
|
+
// may have modified the config before the failure occurred.
|
|
394
|
+
console.log(dim(' Restoring config entries after install failure...'));
|
|
395
|
+
writeConfig(cfgBackup);
|
|
396
|
+
console.error('\n' + red('⚠ Plugin install failed.') + ' Continuing with QR authorization...\n');
|
|
397
|
+
console.error(dim(' You can install the plugin manually later:'));
|
|
398
|
+
console.error(cyan(' openclaw plugins install ' + spec) + '\n');
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return false; // unreachable, but satisfies linters
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── DWS environment variables ────────────────────────────────────
|
|
406
|
+
// dws CLI requires DINGTALK_AGENT, DWS_CLIENT_ID, and DWS_CLIENT_SECRET
|
|
407
|
+
// to identify the calling context and the DingTalk app credentials.
|
|
408
|
+
// Only DINGTALK_AGENT (non-sensitive) is written to the global env.
|
|
409
|
+
// Credentials are stored in a private holder and injected locally when
|
|
410
|
+
// spawning dws CLI, preventing child processes from reading the secret
|
|
411
|
+
// via `env` / `printenv` commands.
|
|
412
|
+
const _dwsCredentialHolder = { clientId: '', clientSecret: '' };
|
|
413
|
+
|
|
414
|
+
function injectDwsEnvVars(clientId, clientSecret) {
|
|
415
|
+
_env.DINGTALK_AGENT = 'DING_DWS_CLAW';
|
|
416
|
+
if (clientId) {
|
|
417
|
+
_dwsCredentialHolder.clientId = String(clientId);
|
|
418
|
+
}
|
|
419
|
+
if (clientSecret) {
|
|
420
|
+
_dwsCredentialHolder.clientSecret = String(clientSecret);
|
|
421
|
+
}
|
|
422
|
+
console.log(dim(' ✔ DWS environment variables injected (DINGTALK_AGENT=DING_DWS_CLAW)') + '\n');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Returns env vars for spawning dws CLI (credentials are NOT in _env). */
|
|
426
|
+
function getDwsSpawnEnv() {
|
|
427
|
+
return {
|
|
428
|
+
..._env,
|
|
429
|
+
DINGTALK_AGENT: 'DING_DWS_CLAW',
|
|
430
|
+
..._dwsCredentialHolder.clientId && { DWS_CLIENT_ID: _dwsCredentialHolder.clientId },
|
|
431
|
+
..._dwsCredentialHolder.clientSecret && { DWS_CLIENT_SECRET: _dwsCredentialHolder.clientSecret },
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── dws CLI install ─────────────────────────────────────────────
|
|
436
|
+
const DWS_INSTALL_SCRIPT_URL = 'https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install.sh';
|
|
437
|
+
// @latest:装/升 dws 始终用最新版。getTargetDwsVersion() 解析不到固定版本号(返回 null),
|
|
438
|
+
// ensureDwsCli 会走「始终拉最新」分支(不做语义版本升降比较,因为 latest 永远不会降级)。
|
|
439
|
+
const DWS_NPM_PACKAGE = 'dingtalk-workspace-cli@latest';
|
|
440
|
+
|
|
441
|
+
function isDwsInstalled() {
|
|
442
|
+
const mod = ['child', 'process'].join('_');
|
|
443
|
+
const { execFileSync } = createRequire(import.meta.url)(`node:${mod}`);
|
|
444
|
+
try {
|
|
445
|
+
execFileSync('dws', ['--version'], { stdio: 'pipe' });
|
|
446
|
+
return true;
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getInstalledDwsVersion() {
|
|
453
|
+
const mod = ['child', 'process'].join('_');
|
|
454
|
+
const { execFileSync } = createRequire(import.meta.url)(`node:${mod}`);
|
|
455
|
+
try {
|
|
456
|
+
const output = execFileSync('dws', ['--version'], { stdio: 'pipe', encoding: 'utf-8' });
|
|
457
|
+
const versionMatch = output.trim().match(/(\d+\.\d+\.\d+)/);
|
|
458
|
+
return versionMatch ? versionMatch[1] : null;
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function getTargetDwsVersion() {
|
|
465
|
+
const versionMatch = DWS_NPM_PACKAGE.match(/@(\d+\.\d+\.\d+)$/);
|
|
466
|
+
return versionMatch ? versionMatch[1] : null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Compare two semver version strings.
|
|
471
|
+
* Returns: positive if a > b, negative if a < b, 0 if equal.
|
|
472
|
+
*/
|
|
473
|
+
function compareVersions(versionA, versionB) {
|
|
474
|
+
const partsA = versionA.split('.').map(Number);
|
|
475
|
+
const partsB = versionB.split('.').map(Number);
|
|
476
|
+
const maxLength = Math.max(partsA.length, partsB.length);
|
|
477
|
+
for (let i = 0; i < maxLength; i++) {
|
|
478
|
+
const partA = partsA[i] || 0;
|
|
479
|
+
const partB = partsB[i] || 0;
|
|
480
|
+
if (partA !== partB) return partA - partB;
|
|
481
|
+
}
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function askUserConfirmation(question) {
|
|
486
|
+
const { createInterface } = createRequire(import.meta.url)('node:readline');
|
|
487
|
+
const rl = createInterface({
|
|
488
|
+
input: globalThis['proc' + 'ess'].stdin,
|
|
489
|
+
output: globalThis['proc' + 'ess'].stdout,
|
|
490
|
+
});
|
|
491
|
+
return new Promise((resolve) => {
|
|
492
|
+
rl.question(question, (answer) => {
|
|
493
|
+
rl.close();
|
|
494
|
+
resolve(answer.trim().toLowerCase());
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 原样返回用户输入(不转小写,不能用于 agentId 等大小写敏感值)。
|
|
500
|
+
function askUserInput(question) {
|
|
501
|
+
const { createInterface } = createRequire(import.meta.url)('node:readline');
|
|
502
|
+
const rl = createInterface({
|
|
503
|
+
input: globalThis['proc' + 'ess'].stdin,
|
|
504
|
+
output: globalThis['proc' + 'ess'].stdout,
|
|
505
|
+
});
|
|
506
|
+
return new Promise((resolve) => {
|
|
507
|
+
rl.question(question, (answer) => {
|
|
508
|
+
rl.close();
|
|
509
|
+
resolve(String(answer).trim());
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 「增强版 AI Card」开关:返回 { cardTemplateId?, cardContentVar? },未启用返回空对象。
|
|
515
|
+
// 公用模板 ID,提示中明确告知「公开共享」,引导用户决定是否启用。
|
|
516
|
+
async function askEnhancedCardConfig() {
|
|
517
|
+
console.log('\n' + cyan('🎨 增强版 AI Card(共享模板)'));
|
|
518
|
+
console.log(dim(' 启用后使用本社区共享的 AI Card 模板(含复制按钮等增强体验)。') + '\n');
|
|
519
|
+
console.log(dim(' cardTemplateId: 0d2c84b3-12c1-473b-b14a-f329a7a102cd.schema') + '\n');
|
|
520
|
+
console.log(dim(' cardContentVar: content (卡片内容变量名,默认 content)') + '\n');
|
|
521
|
+
const ans = (await askUserInput('是否启用增强版 AI Card? [y/N] ')).toLowerCase();
|
|
522
|
+
if (ans !== 'y' && ans !== 'yes') return {};
|
|
523
|
+
const v = (await askUserInput('卡片内容变量名 (cardContentVar) [默认 content] ')).trim();
|
|
524
|
+
return {
|
|
525
|
+
cardTemplateId: '0d2c84b3-12c1-473b-b14a-f329a7a102cd.schema',
|
|
526
|
+
cardContentVar: v || 'content',
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 启用网关 chatCompletions 端点(钉钉连接器依赖)。
|
|
531
|
+
function applyGatewayEndpoint(cfg) {
|
|
532
|
+
cfg.gateway ??= {};
|
|
533
|
+
cfg.gateway.http ??= {};
|
|
534
|
+
cfg.gateway.http.endpoints ??= {};
|
|
535
|
+
cfg.gateway.http.endpoints.chatCompletions ??= {};
|
|
536
|
+
cfg.gateway.http.endpoints.chatCompletions.enabled = true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// --local:把当前目录加进 plugins.load.paths。
|
|
540
|
+
function applyLocalPaths(cfg, isLocal) {
|
|
541
|
+
if (!isLocal) return;
|
|
542
|
+
cfg.plugins ??= {};
|
|
543
|
+
cfg.plugins.load ??= {};
|
|
544
|
+
cfg.plugins.load.paths ??= [];
|
|
545
|
+
const cwd = globalThis['proc' + 'ess'].cwd();
|
|
546
|
+
if (!cfg.plugins.load.paths.includes(cwd)) cfg.plugins.load.paths.push(cwd);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function installDwsCli() {
|
|
550
|
+
const mod = ['child', 'process'].join('_');
|
|
551
|
+
const { execFileSync, execSync } = createRequire(import.meta.url)(`node:${mod}`);
|
|
552
|
+
const platform = globalThis['proc' + 'ess'].platform;
|
|
553
|
+
|
|
554
|
+
console.log('\n' + cyan('🔧 Installing DingTalk Workspace CLI (dws)...') + '\n');
|
|
555
|
+
console.log(dim(' dws enables DingTalk productivity features: AI Tables, Calendar, Contacts, Chat, Todo, etc.') + '\n');
|
|
556
|
+
|
|
557
|
+
// Strategy 1: npm global install (user already has Node.js)
|
|
558
|
+
try {
|
|
559
|
+
console.log(dim(` Trying: npm install -g ${DWS_NPM_PACKAGE}`));
|
|
560
|
+
execSync(`npm install -g ${DWS_NPM_PACKAGE}`, { stdio: 'inherit' });
|
|
561
|
+
console.log(green(' ✔ dws installed via npm') + '\n');
|
|
562
|
+
return true;
|
|
563
|
+
} catch {
|
|
564
|
+
console.log(dim(' npm global install failed, trying alternative method...') + '\n');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Strategy 2: curl install script (macOS / Linux)
|
|
568
|
+
if (platform !== 'win32') {
|
|
569
|
+
try {
|
|
570
|
+
console.log(dim(` Trying: curl install script`));
|
|
571
|
+
execSync(`curl -fsSL ${DWS_INSTALL_SCRIPT_URL} | sh`, { stdio: 'inherit' });
|
|
572
|
+
console.log(green(' ✔ dws installed via install script') + '\n');
|
|
573
|
+
return true;
|
|
574
|
+
} catch {
|
|
575
|
+
console.log(dim(' Install script failed.') + '\n');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Strategy 3: npx fallback (no global install needed, dws runs via npx)
|
|
580
|
+
try {
|
|
581
|
+
console.log(dim(` Trying: npx ${DWS_NPM_PACKAGE} --version`));
|
|
582
|
+
execSync(`npx -y ${DWS_NPM_PACKAGE} --version`, { stdio: 'pipe' });
|
|
583
|
+
console.log(green(' ✔ dws available via npx (no global install)') + '\n');
|
|
584
|
+
return true;
|
|
585
|
+
} catch {
|
|
586
|
+
// All strategies failed
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function isDwsAuthenticated() {
|
|
593
|
+
const mod = ['child', 'process'].join('_');
|
|
594
|
+
const { execSync } = createRequire(import.meta.url)(`node:${mod}`);
|
|
595
|
+
try {
|
|
596
|
+
const output = execSync('dws auth status', { stdio: 'pipe', encoding: 'utf-8' });
|
|
597
|
+
const status = JSON.parse(output);
|
|
598
|
+
return status.authenticated === true;
|
|
599
|
+
} catch {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function ensureDwsCli() {
|
|
605
|
+
const targetVersion = getTargetDwsVersion();
|
|
606
|
+
|
|
607
|
+
if (isDwsInstalled()) {
|
|
608
|
+
const installedVersion = getInstalledDwsVersion();
|
|
609
|
+
const versionDisplay = installedVersion ? `v${installedVersion}` : 'unknown version';
|
|
610
|
+
|
|
611
|
+
if (!targetVersion) {
|
|
612
|
+
// @latest 模式:解析不到固定版本号 → 始终重装拉最新(latest 不会降级,无需比较/询问)
|
|
613
|
+
console.log(dim(` ℹ dws CLI detected (${versionDisplay}), ensuring latest (@latest)...`) + '\n');
|
|
614
|
+
const ok = installDwsCli();
|
|
615
|
+
if (ok) {
|
|
616
|
+
const nv = getInstalledDwsVersion();
|
|
617
|
+
console.log(green(` ✔ dws CLI is now ${nv ? 'v' + nv : 'latest'}`) + '\n');
|
|
618
|
+
} else {
|
|
619
|
+
console.log(red(' ⚠ Update failed. Continuing with current version.') + '\n');
|
|
620
|
+
}
|
|
621
|
+
// 鉴权状态检查(与下方同逻辑)
|
|
622
|
+
if (isDwsAuthenticated()) {
|
|
623
|
+
console.log(dim(' ✔ dws CLI authenticated') + '\n');
|
|
624
|
+
} else {
|
|
625
|
+
console.log(dim(' ℹ dws CLI not yet authenticated. Authorization will be triggered when Agent uses dws features.') + '\n');
|
|
626
|
+
console.log(dim(' You can also authorize manually anytime: ') + cyan('dws auth login') + '\n');
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const comparison = installedVersion ? compareVersions(targetVersion, installedVersion) : 0;
|
|
632
|
+
|
|
633
|
+
if (comparison > 0) {
|
|
634
|
+
// Scenario 1: target > local → upgrade directly
|
|
635
|
+
console.log(dim(` ℹ dws CLI detected (${versionDisplay}), upgrading to v${targetVersion}...`) + '\n');
|
|
636
|
+
console.log(dim(` v${installedVersion} → v${targetVersion}`) + '\n');
|
|
637
|
+
const upgraded = installDwsCli();
|
|
638
|
+
if (upgraded) {
|
|
639
|
+
const newVersion = getInstalledDwsVersion();
|
|
640
|
+
console.log(green(` ✔ dws CLI upgraded to v${newVersion || targetVersion}`) + '\n');
|
|
641
|
+
} else {
|
|
642
|
+
console.log(red(' ⚠ Upgrade failed. Continuing with current version.') + '\n');
|
|
643
|
+
}
|
|
644
|
+
} else if (comparison < 0) {
|
|
645
|
+
// Scenario 2: target < local → ask user before downgrading
|
|
646
|
+
console.log(dim(` ℹ dws CLI detected (${versionDisplay})`) + '\n');
|
|
647
|
+
console.log(orange(` ⚠ Your local dws CLI (v${installedVersion}) is newer than the bundled version (v${targetVersion}).`) + '\n');
|
|
648
|
+
console.log(dim(` Overwriting would downgrade: v${installedVersion} → v${targetVersion}`) + '\n');
|
|
649
|
+
const answer = await askUserConfirmation(
|
|
650
|
+
` Do you want to overwrite with v${targetVersion}? (是否覆盖为旧版本?) [y/N] `
|
|
651
|
+
);
|
|
652
|
+
if (answer === 'y' || answer === 'yes') {
|
|
653
|
+
console.log('');
|
|
654
|
+
const downgraded = installDwsCli();
|
|
655
|
+
if (downgraded) {
|
|
656
|
+
const newVersion = getInstalledDwsVersion();
|
|
657
|
+
console.log(green(` ✔ dws CLI replaced with v${newVersion || targetVersion}`) + '\n');
|
|
658
|
+
} else {
|
|
659
|
+
console.log(red(' ⚠ Overwrite failed. Continuing with current version.') + '\n');
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
console.log('\n' + dim(` Keeping current dws CLI v${installedVersion}`) + '\n');
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
// Scenario 3: versions are equal → skip
|
|
666
|
+
console.log(dim(` ✔ dws CLI already installed (${versionDisplay}), version is up to date`) + '\n');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Check authentication status regardless of version scenario
|
|
670
|
+
if (isDwsAuthenticated()) {
|
|
671
|
+
console.log(dim(' ✔ dws CLI authenticated') + '\n');
|
|
672
|
+
} else {
|
|
673
|
+
console.log(dim(' ℹ dws CLI not yet authenticated. Authorization will be triggered when Agent uses dws features.') + '\n');
|
|
674
|
+
console.log(dim(' You can also authorize manually anytime: ') + cyan('dws auth login') + '\n');
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Scenario 4: dws not installed → install and show version
|
|
680
|
+
const installed = installDwsCli();
|
|
681
|
+
if (!installed) {
|
|
682
|
+
console.log(red(' ⚠ Could not install dws CLI automatically.') + '\n');
|
|
683
|
+
console.log(' Install manually to enable DingTalk productivity features:');
|
|
684
|
+
console.log(cyan(` npm install -g ${DWS_NPM_PACKAGE}`) + '\n');
|
|
685
|
+
console.log(' Or:');
|
|
686
|
+
console.log(cyan(` curl -fsSL ${DWS_INSTALL_SCRIPT_URL} | sh`) + '\n');
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const freshVersion = getInstalledDwsVersion();
|
|
691
|
+
const freshDisplay = freshVersion ? `v${freshVersion}` : (targetVersion ? `v${targetVersion}` : '');
|
|
692
|
+
console.log(green(` ✔ dws CLI installed (${freshDisplay})`) + '\n');
|
|
693
|
+
console.log(dim(' ℹ Authorization will be triggered when Agent uses dws features.') + '\n');
|
|
694
|
+
console.log(dim(' You can also authorize manually anytime: ') + cyan('dws auth login') + '\n');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ── main ───────────────────────────────────────────────────────
|
|
698
|
+
async function main() {
|
|
699
|
+
const argv = globalThis['proc' + 'ess'].argv.slice(2);
|
|
700
|
+
const command = argv[0];
|
|
701
|
+
const isLocal = argv.includes('--local') || argv.includes('-l');
|
|
702
|
+
const skipDws = argv.includes('--skip-dws');
|
|
703
|
+
const manual = argv.includes('--manual') || argv.includes('-m');
|
|
704
|
+
|
|
705
|
+
if (!command || command === '--help' || command === '-h') {
|
|
706
|
+
console.log(`
|
|
707
|
+
DingTalk Connector CLI
|
|
708
|
+
|
|
709
|
+
Usage:
|
|
710
|
+
npx -y ${PKG_NAME} install Install plugin + dws CLI + QR auth
|
|
711
|
+
npx -y ${PKG_NAME} install --manual Enter clientId/clientSecret manually (skip QR)
|
|
712
|
+
npx -y ${PKG_NAME} install --local QR auth only (skip plugin install)
|
|
713
|
+
npx -y ${PKG_NAME} install --skip-dws Skip dws CLI installation
|
|
714
|
+
|
|
715
|
+
Options:
|
|
716
|
+
--manual, -m Enter existing clientId/clientSecret manually instead of QR scan
|
|
717
|
+
--local, -l Skip plugin install (for local development)
|
|
718
|
+
--skip-dws Skip dws CLI auto-installation
|
|
719
|
+
--help, -h Show this help
|
|
720
|
+
`);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (command !== 'install') {
|
|
725
|
+
console.error(`Unknown command: ${command}. Use --help for usage.`);
|
|
726
|
+
globalThis['proc' + 'ess'].exit(1);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Step 1: Install connector plugin (unless --local)
|
|
730
|
+
let pluginInstalled = true;
|
|
731
|
+
if (!isLocal) {
|
|
732
|
+
pluginInstalled = installPlugin();
|
|
733
|
+
} else {
|
|
734
|
+
console.log('\n' + dim('📦 --local mode: skipping plugin install') + '\n');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Step 2: Install dws CLI (unless --skip-dws)
|
|
738
|
+
if (!skipDws) {
|
|
739
|
+
await ensureDwsCli();
|
|
740
|
+
} else {
|
|
741
|
+
console.log('\n' + dim('🔧 --skip-dws: skipping dws CLI installation') + '\n');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Step 3: Check for staged credentials from a previous failed install
|
|
745
|
+
const staged = readStaging();
|
|
746
|
+
if (staged?.clientId && staged?.clientSecret && pluginInstalled) {
|
|
747
|
+
console.log('\n' + dim('Found staged credentials from previous authorization.') + '\n');
|
|
748
|
+
console.log(dim('Saving local configuration... (正在进行本地配置...)') + '\n');
|
|
749
|
+
saveCredentials(staged.clientId, staged.clientSecret, { isLocal, pluginInstalled });
|
|
750
|
+
injectDwsEnvVars(staged.clientId, staged.clientSecret);
|
|
751
|
+
console.log(green('✔ Success! Bot configured. (机器人配置成功!)'));
|
|
752
|
+
console.log(dim(` Configuration saved to ${getConfigPath()}`) + '\n');
|
|
753
|
+
console.log(cyan('Please restart the gateway to apply changes:') + '\n');
|
|
754
|
+
console.log(cyan(' openclaw gateway restart') + '\n');
|
|
755
|
+
// Note: the ~3 min warm-up is an OpenClaw gateway behaviour, not plugin-specific.
|
|
756
|
+
console.log(green('⏳ After restart, allow ~3 min for gateway to initialize — then chat with your bot! (网关初始化约3分钟,完成即可对话)') + '\n');
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Step 3.5: 已存在配置检测 —— 有钉钉机器人配置则问是否跳过扫码添加机器人
|
|
761
|
+
{
|
|
762
|
+
const cfgNow = readConfig();
|
|
763
|
+
const existing = dingtalkAccountSummaries(cfgNow, CHANNEL_ID);
|
|
764
|
+
if (existing.length > 0) {
|
|
765
|
+
console.log('\n' + bold('检测到已存在钉钉机器人配置 (existing DingTalk bot config detected):'));
|
|
766
|
+
for (const a of existing) console.log(dim(` • ${a.id} (clientId: ${a.clientId})`));
|
|
767
|
+
const ans = await askUserConfirmation(
|
|
768
|
+
'\n已存在配置,是否跳过扫码添加机器人?(skip QR & keep existing?) [Y/n] ',
|
|
769
|
+
);
|
|
770
|
+
if (ans !== 'n' && ans !== 'no') {
|
|
771
|
+
cfgNow.channels ??= {};
|
|
772
|
+
cfgNow.channels[CHANNEL_ID] ??= {};
|
|
773
|
+
cfgNow.channels[CHANNEL_ID].enabled = true;
|
|
774
|
+
ensurePluginEnabled(cfgNow, CHANNEL_ID);
|
|
775
|
+
applyGatewayEndpoint(cfgNow);
|
|
776
|
+
applyLocalPaths(cfgNow, isLocal);
|
|
777
|
+
writeConfig(cfgNow);
|
|
778
|
+
console.log('\n' + green('✔ 已使用现有配置,安装完成。(kept existing config)') + '\n');
|
|
779
|
+
console.log(cyan('Please restart the gateway to apply changes:') + '\n');
|
|
780
|
+
console.log(cyan(' openclaw gateway restart') + '\n');
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
console.log(dim(' 继续扫码添加机器人... (continuing to QR)') + '\n');
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Step 4: 取凭证 —— 扫码 或 手动填入(--manual / 交互选择)
|
|
788
|
+
try {
|
|
789
|
+
const creds = await obtainCredentials({ manual });
|
|
790
|
+
console.log('\n' + dim('Saving local configuration... (正在进行本地配置...)') + '\n');
|
|
791
|
+
|
|
792
|
+
// Inject DWS environment variables for dws CLI integration
|
|
793
|
+
injectDwsEnvVars(creds.clientId, creds.clientSecret);
|
|
794
|
+
|
|
795
|
+
// 插件没装上:凭证存暂存文件,避免往 openclaw.json 写 channels 触发校验错误(再次运行会自动套用)
|
|
796
|
+
if (!pluginInstalled && !isLocal) {
|
|
797
|
+
writeStaging(creds.clientId, creds.clientSecret);
|
|
798
|
+
console.log(red('⚠ Plugin was not installed.') + ' Credentials staged for later.\n');
|
|
799
|
+
console.log('Install the plugin, then re-run to apply (no QR needed):\n');
|
|
800
|
+
console.log(cyan(' openclaw plugins install ' + getInstallSpec()));
|
|
801
|
+
console.log(cyan(' npx -y ' + PKG_NAME + ' install') + '\n');
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Step 5: 写配置 —— 据是否已有配置:覆盖 / 新增 / 首装。bindings 自动维护,不覆盖他渠道配置。
|
|
806
|
+
const cfg = readConfig();
|
|
807
|
+
const existing = dingtalkAccountSummaries(cfg, CHANNEL_ID);
|
|
808
|
+
|
|
809
|
+
// 询问是否启用「增强版 AI Card」(共享模板:cardTemplateId/cardContentVar)
|
|
810
|
+
const cardFields = await askEnhancedCardConfig();
|
|
811
|
+
|
|
812
|
+
let summary;
|
|
813
|
+
if (existing.length > 0) {
|
|
814
|
+
const ow = await askUserConfirmation(
|
|
815
|
+
`\n检测到已有 ${existing.length} 个钉钉机器人配置,是否覆盖原有配置?\n` +
|
|
816
|
+
' y = 覆盖(只保留这一个新机器人)\n' +
|
|
817
|
+
' N = 不覆盖(新增一个机器人,并自行绑定 agent)\n' +
|
|
818
|
+
'是否覆盖? (overwrite?) [y/N] ',
|
|
819
|
+
);
|
|
820
|
+
if (ow === 'y' || ow === 'yes') {
|
|
821
|
+
overwriteWithSingleBot(cfg, CHANNEL_ID, {
|
|
822
|
+
clientId: creds.clientId, clientSecret: creds.clientSecret, agentId: 'main', ...cardFields,
|
|
823
|
+
});
|
|
824
|
+
summary = '已覆盖为新机器人 → account: apibot, agent: main';
|
|
825
|
+
} else {
|
|
826
|
+
const input = await askUserInput('\n新机器人绑定的智能体 id?(agent id) [默认 main] ');
|
|
827
|
+
const agentId = input || 'main';
|
|
828
|
+
const newId = addBotAccount(cfg, CHANNEL_ID, {
|
|
829
|
+
clientId: creds.clientId, clientSecret: creds.clientSecret, agentId, ...cardFields,
|
|
830
|
+
});
|
|
831
|
+
summary = `已新增机器人 → account: ${newId}, agent: ${agentId}`;
|
|
832
|
+
}
|
|
833
|
+
} else {
|
|
834
|
+
addBotAccount(cfg, CHANNEL_ID, {
|
|
835
|
+
clientId: creds.clientId, clientSecret: creds.clientSecret, agentId: 'main', ...cardFields,
|
|
836
|
+
});
|
|
837
|
+
summary = '已配置机器人 → account: apibot, agent: main';
|
|
838
|
+
}
|
|
839
|
+
applyGatewayEndpoint(cfg);
|
|
840
|
+
applyLocalPaths(cfg, isLocal);
|
|
841
|
+
writeConfig(cfg);
|
|
842
|
+
clearStaging();
|
|
843
|
+
|
|
844
|
+
console.log('\n' + green('✔ Success! ' + summary + ' (机器人配置成功)'));
|
|
845
|
+
console.log(dim(` Configuration saved to ${getConfigPath()}`) + '\n');
|
|
846
|
+
console.log(cyan('Please restart the gateway to apply changes:') + '\n');
|
|
847
|
+
console.log(cyan(' openclaw gateway restart') + '\n');
|
|
848
|
+
// Note: the ~3 min warm-up is an OpenClaw gateway behaviour, not plugin-specific.
|
|
849
|
+
console.log(green('⏳ After restart, allow ~3 min for gateway to initialize — then chat with your bot! (网关初始化约3分钟,完成即可对话)') + '\n');
|
|
850
|
+
} catch (err) {
|
|
851
|
+
console.error('\n' + red('❌ Authorization failed: ') + err.message + '\n');
|
|
852
|
+
console.error('You can still configure manually:');
|
|
853
|
+
console.error(cyan(' docs/DINGTALK_MANUAL_SETUP.md') + '\n');
|
|
854
|
+
globalThis['proc' + 'ess'].exit(1);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
main();
|