@openclaw-china/shared 0.1.35 → 0.1.37
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/package.json +1 -1
- package/src/cli/china-setup.ts +303 -243
- package/src/cli/index.ts +2 -2
- package/src/cli/install-hint.ts +95 -95
- package/src/file/file-utils.test.ts +141 -141
- package/src/file/file-utils.ts +284 -284
- package/src/file/index.ts +10 -10
- package/src/index.ts +3 -3
- package/src/logger/index.ts +1 -1
- package/src/logger/logger.ts +51 -51
- package/src/media/index.ts +22 -22
- package/src/media/media-io.ts +328 -328
- package/vitest.config.ts +8 -8
package/package.json
CHANGED
package/src/cli/china-setup.ts
CHANGED
|
@@ -38,50 +38,50 @@ type RegisterCliLike = (
|
|
|
38
38
|
|
|
39
39
|
type WriteConfigLike = (cfg: ConfigRoot) => Promise<void>;
|
|
40
40
|
|
|
41
|
-
type ApiLike = {
|
|
42
|
-
registerCli?: RegisterCliLike;
|
|
43
|
-
runtime?: unknown;
|
|
44
|
-
logger?: LoggerLike;
|
|
45
|
-
};
|
|
41
|
+
type ApiLike = {
|
|
42
|
+
registerCli?: RegisterCliLike;
|
|
43
|
+
runtime?: unknown;
|
|
44
|
+
logger?: LoggerLike;
|
|
45
|
+
};
|
|
46
46
|
|
|
47
47
|
type ConfigRecord = Record<string, unknown>;
|
|
48
48
|
|
|
49
|
-
type ConfigRoot = {
|
|
50
|
-
channels?: ConfigRecord;
|
|
51
|
-
[key: string]: unknown;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export type ChannelId = "dingtalk" | "feishu-china" | "wecom" | "wecom-app" | "qqbot";
|
|
55
|
-
|
|
56
|
-
export type RegisterChinaSetupCliOptions = {
|
|
57
|
-
channels?: readonly ChannelId[];
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type Option<T extends string> = {
|
|
61
|
-
key?: string;
|
|
62
|
-
value: T;
|
|
63
|
-
label: string;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const PROJECT_REPO = "https://github.com/BytePioneer-AI/
|
|
67
|
-
const GUIDES_BASE = "https://github.com/BytePioneer-AI/openclaw-china/tree/main/doc/guides";
|
|
68
|
-
const OPENCLAW_HOME = join(homedir(), ".openclaw");
|
|
69
|
-
const DEFAULT_PLUGIN_PATH = join(OPENCLAW_HOME, "extensions");
|
|
70
|
-
const LEGACY_PLUGIN_PATH = join(OPENCLAW_HOME, "plugins");
|
|
71
|
-
const CONFIG_FILE_PATH = join(OPENCLAW_HOME, "openclaw.json");
|
|
72
|
-
const ANSI_RESET = "\u001b[0m";
|
|
73
|
-
const ANSI_LINK = "\u001b[1;4;96m";
|
|
74
|
-
const ANSI_BORDER = "\u001b[92m";
|
|
75
|
-
const CHANNEL_ORDER: readonly ChannelId[] = [
|
|
76
|
-
"dingtalk",
|
|
77
|
-
"qqbot",
|
|
78
|
-
"wecom",
|
|
79
|
-
"wecom-app",
|
|
80
|
-
"feishu-china",
|
|
81
|
-
];
|
|
82
|
-
const CHANNEL_DISPLAY_LABELS: Record<ChannelId, string> = {
|
|
83
|
-
dingtalk: "DingTalk(钉钉)",
|
|
84
|
-
"feishu-china": "Feishu(飞书)",
|
|
49
|
+
type ConfigRoot = {
|
|
50
|
+
channels?: ConfigRecord;
|
|
51
|
+
[key: string]: unknown;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ChannelId = "dingtalk" | "feishu-china" | "wecom" | "wecom-app" | "qqbot";
|
|
55
|
+
|
|
56
|
+
export type RegisterChinaSetupCliOptions = {
|
|
57
|
+
channels?: readonly ChannelId[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type Option<T extends string> = {
|
|
61
|
+
key?: string;
|
|
62
|
+
value: T;
|
|
63
|
+
label: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const PROJECT_REPO = "https://github.com/BytePioneer-AI/openclaw-china";
|
|
67
|
+
const GUIDES_BASE = "https://github.com/BytePioneer-AI/openclaw-china/tree/main/doc/guides";
|
|
68
|
+
const OPENCLAW_HOME = join(homedir(), ".openclaw");
|
|
69
|
+
const DEFAULT_PLUGIN_PATH = join(OPENCLAW_HOME, "extensions");
|
|
70
|
+
const LEGACY_PLUGIN_PATH = join(OPENCLAW_HOME, "plugins");
|
|
71
|
+
const CONFIG_FILE_PATH = join(OPENCLAW_HOME, "openclaw.json");
|
|
72
|
+
const ANSI_RESET = "\u001b[0m";
|
|
73
|
+
const ANSI_LINK = "\u001b[1;4;96m";
|
|
74
|
+
const ANSI_BORDER = "\u001b[92m";
|
|
75
|
+
const CHANNEL_ORDER: readonly ChannelId[] = [
|
|
76
|
+
"dingtalk",
|
|
77
|
+
"qqbot",
|
|
78
|
+
"wecom",
|
|
79
|
+
"wecom-app",
|
|
80
|
+
"feishu-china",
|
|
81
|
+
];
|
|
82
|
+
const CHANNEL_DISPLAY_LABELS: Record<ChannelId, string> = {
|
|
83
|
+
dingtalk: "DingTalk(钉钉)",
|
|
84
|
+
"feishu-china": "Feishu(飞书)",
|
|
85
85
|
wecom: "WeCom(企业微信-智能机器人)",
|
|
86
86
|
"wecom-app": "WeCom App(自建应用-可接入微信)",
|
|
87
87
|
qqbot: "QQBot(QQ 机器人)",
|
|
@@ -90,71 +90,71 @@ const CHANNEL_GUIDE_LINKS: Record<ChannelId, string> = {
|
|
|
90
90
|
dingtalk: `${GUIDES_BASE}/dingtalk/configuration.md`,
|
|
91
91
|
"feishu-china": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/README.md",
|
|
92
92
|
wecom: `${GUIDES_BASE}/wecom/configuration.md`,
|
|
93
|
-
"wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
|
|
94
|
-
qqbot: `${GUIDES_BASE}/qqbot/configuration.md`,
|
|
95
|
-
};
|
|
96
|
-
const CHINA_CLI_STATE_KEY = Symbol.for("@openclaw-china/china-cli-state");
|
|
97
|
-
|
|
98
|
-
type ChinaCliState = {
|
|
99
|
-
channels: Set<ChannelId>;
|
|
100
|
-
cliRegistered: boolean;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
class PromptCancelledError extends Error {
|
|
104
|
-
constructor() {
|
|
105
|
-
super("prompt-cancelled");
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function isChannelId(value: unknown): value is ChannelId {
|
|
110
|
-
return typeof value === "string" && CHANNEL_ORDER.includes(value as ChannelId);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function getChinaCliState(): ChinaCliState {
|
|
114
|
-
const root = globalThis as Record<PropertyKey, unknown>;
|
|
115
|
-
const cached = root[CHINA_CLI_STATE_KEY];
|
|
116
|
-
|
|
117
|
-
if (isRecord(cached)) {
|
|
118
|
-
const channels = cached.channels;
|
|
119
|
-
const cliRegistered = cached.cliRegistered;
|
|
120
|
-
if (channels instanceof Set && typeof cliRegistered === "boolean") {
|
|
121
|
-
return {
|
|
122
|
-
channels: channels as Set<ChannelId>,
|
|
123
|
-
cliRegistered,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const created: ChinaCliState = {
|
|
129
|
-
channels: new Set<ChannelId>(),
|
|
130
|
-
cliRegistered: false,
|
|
131
|
-
};
|
|
132
|
-
root[CHINA_CLI_STATE_KEY] = created;
|
|
133
|
-
return created;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function normalizeChannels(channels?: readonly ChannelId[]): ChannelId[] {
|
|
137
|
-
const selected = channels && channels.length > 0 ? channels : CHANNEL_ORDER;
|
|
138
|
-
const unique = new Set<ChannelId>();
|
|
139
|
-
for (const channelId of selected) {
|
|
140
|
-
if (isChannelId(channelId)) {
|
|
141
|
-
unique.add(channelId);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return CHANNEL_ORDER.filter((channelId) => unique.has(channelId));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function getInstalledChannels(state: ChinaCliState): ChannelId[] {
|
|
148
|
-
return CHANNEL_ORDER.filter((channelId) => state.channels.has(channelId));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function guardCancel<T>(value: T | symbol): T {
|
|
152
|
-
if (isCancel(value)) {
|
|
153
|
-
clackCancel("已取消配置。");
|
|
154
|
-
throw new PromptCancelledError();
|
|
155
|
-
}
|
|
156
|
-
return value as T;
|
|
157
|
-
}
|
|
93
|
+
"wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
|
|
94
|
+
qqbot: `${GUIDES_BASE}/qqbot/configuration.md`,
|
|
95
|
+
};
|
|
96
|
+
const CHINA_CLI_STATE_KEY = Symbol.for("@openclaw-china/china-cli-state");
|
|
97
|
+
|
|
98
|
+
type ChinaCliState = {
|
|
99
|
+
channels: Set<ChannelId>;
|
|
100
|
+
cliRegistered: boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
class PromptCancelledError extends Error {
|
|
104
|
+
constructor() {
|
|
105
|
+
super("prompt-cancelled");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isChannelId(value: unknown): value is ChannelId {
|
|
110
|
+
return typeof value === "string" && CHANNEL_ORDER.includes(value as ChannelId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getChinaCliState(): ChinaCliState {
|
|
114
|
+
const root = globalThis as Record<PropertyKey, unknown>;
|
|
115
|
+
const cached = root[CHINA_CLI_STATE_KEY];
|
|
116
|
+
|
|
117
|
+
if (isRecord(cached)) {
|
|
118
|
+
const channels = cached.channels;
|
|
119
|
+
const cliRegistered = cached.cliRegistered;
|
|
120
|
+
if (channels instanceof Set && typeof cliRegistered === "boolean") {
|
|
121
|
+
return {
|
|
122
|
+
channels: channels as Set<ChannelId>,
|
|
123
|
+
cliRegistered,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const created: ChinaCliState = {
|
|
129
|
+
channels: new Set<ChannelId>(),
|
|
130
|
+
cliRegistered: false,
|
|
131
|
+
};
|
|
132
|
+
root[CHINA_CLI_STATE_KEY] = created;
|
|
133
|
+
return created;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeChannels(channels?: readonly ChannelId[]): ChannelId[] {
|
|
137
|
+
const selected = channels && channels.length > 0 ? channels : CHANNEL_ORDER;
|
|
138
|
+
const unique = new Set<ChannelId>();
|
|
139
|
+
for (const channelId of selected) {
|
|
140
|
+
if (isChannelId(channelId)) {
|
|
141
|
+
unique.add(channelId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return CHANNEL_ORDER.filter((channelId) => unique.has(channelId));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getInstalledChannels(state: ChinaCliState): ChannelId[] {
|
|
148
|
+
return CHANNEL_ORDER.filter((channelId) => state.channels.has(channelId));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function guardCancel<T>(value: T | symbol): T {
|
|
152
|
+
if (isCancel(value)) {
|
|
153
|
+
clackCancel("已取消配置。");
|
|
154
|
+
throw new PromptCancelledError();
|
|
155
|
+
}
|
|
156
|
+
return value as T;
|
|
157
|
+
}
|
|
158
158
|
|
|
159
159
|
function warn(text: string): void {
|
|
160
160
|
output.write(`\n[warn] ${text}\n`);
|
|
@@ -174,25 +174,25 @@ function resolvePluginPath(): string {
|
|
|
174
174
|
return DEFAULT_PLUGIN_PATH;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
function renderReadyMessage(): string {
|
|
178
|
-
return [
|
|
179
|
-
`${ANSI_BORDER}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI_RESET}`,
|
|
180
|
-
" OpenClaw China Channels 已就绪!",
|
|
181
|
-
`${ANSI_BORDER}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI_RESET}`,
|
|
177
|
+
function renderReadyMessage(): string {
|
|
178
|
+
return [
|
|
179
|
+
`${ANSI_BORDER}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI_RESET}`,
|
|
180
|
+
" OpenClaw China Channels 已就绪!",
|
|
181
|
+
`${ANSI_BORDER}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI_RESET}`,
|
|
182
182
|
"",
|
|
183
183
|
"插件路径:",
|
|
184
184
|
` ${resolvePluginPath()}`,
|
|
185
185
|
"",
|
|
186
|
-
"配置文件:",
|
|
187
|
-
` ${CONFIG_FILE_PATH}`,
|
|
188
|
-
"",
|
|
189
|
-
"更新插件:",
|
|
190
|
-
" openclaw plugins update <plugin-id>",
|
|
191
|
-
"",
|
|
192
|
-
"项目仓库:",
|
|
193
|
-
` ${ANSI_LINK}${PROJECT_REPO}${ANSI_RESET}`,
|
|
186
|
+
"配置文件:",
|
|
187
|
+
` ${CONFIG_FILE_PATH}`,
|
|
194
188
|
"",
|
|
195
|
-
"
|
|
189
|
+
"更新插件:",
|
|
190
|
+
" openclaw plugins update <plugin-id>",
|
|
191
|
+
"",
|
|
192
|
+
"项目仓库:",
|
|
193
|
+
` ${ANSI_LINK}${PROJECT_REPO}${ANSI_RESET}`,
|
|
194
|
+
"",
|
|
195
|
+
"⭐ 如果这个项目对你有帮助,请给我们一个 Star!⭐",
|
|
196
196
|
"",
|
|
197
197
|
"下一步:",
|
|
198
198
|
" openclaw gateway --port 18789 --verbose",
|
|
@@ -209,23 +209,23 @@ function showGuideLink(channelId: ChannelId): void {
|
|
|
209
209
|
clackNote(`配置文档:${url}`, "Docs");
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
function isRecord(value: unknown): value is ConfigRecord {
|
|
213
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function resolveWriteConfig(runtime: unknown): WriteConfigLike | undefined {
|
|
217
|
-
if (!isRecord(runtime)) {
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
const config = runtime.config;
|
|
221
|
-
if (!isRecord(config)) {
|
|
222
|
-
return undefined;
|
|
223
|
-
}
|
|
224
|
-
if (typeof config.writeConfigFile !== "function") {
|
|
225
|
-
return undefined;
|
|
226
|
-
}
|
|
227
|
-
return config.writeConfigFile as WriteConfigLike;
|
|
228
|
-
}
|
|
212
|
+
function isRecord(value: unknown): value is ConfigRecord {
|
|
213
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveWriteConfig(runtime: unknown): WriteConfigLike | undefined {
|
|
217
|
+
if (!isRecord(runtime)) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const config = runtime.config;
|
|
221
|
+
if (!isRecord(config)) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
if (typeof config.writeConfigFile !== "function") {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
return config.writeConfigFile as WriteConfigLike;
|
|
228
|
+
}
|
|
229
229
|
|
|
230
230
|
function isCommandLike(value: unknown): value is CommandLike {
|
|
231
231
|
if (!isRecord(value)) {
|
|
@@ -424,28 +424,28 @@ class SetupPrompter {
|
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
async askSelect<T extends string>(
|
|
428
|
-
message: string,
|
|
429
|
-
options: Array<Option<T>>,
|
|
430
|
-
defaultValue: T
|
|
431
|
-
): Promise<T> {
|
|
432
|
-
const initial = options.some((opt) => opt.value === defaultValue)
|
|
433
|
-
? defaultValue
|
|
434
|
-
: options[0]?.value;
|
|
435
|
-
const selectOptions = options.map((option) => ({
|
|
436
|
-
value: option.value,
|
|
437
|
-
label: option.label,
|
|
438
|
-
})) as Parameters<typeof clackSelect<T>>[0]["options"];
|
|
439
|
-
|
|
440
|
-
return guardCancel(
|
|
441
|
-
await clackSelect<T>({
|
|
442
|
-
message,
|
|
443
|
-
options: selectOptions,
|
|
444
|
-
initialValue: initial,
|
|
445
|
-
})
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
427
|
+
async askSelect<T extends string>(
|
|
428
|
+
message: string,
|
|
429
|
+
options: Array<Option<T>>,
|
|
430
|
+
defaultValue: T
|
|
431
|
+
): Promise<T> {
|
|
432
|
+
const initial = options.some((opt) => opt.value === defaultValue)
|
|
433
|
+
? defaultValue
|
|
434
|
+
: options[0]?.value;
|
|
435
|
+
const selectOptions = options.map((option) => ({
|
|
436
|
+
value: option.value,
|
|
437
|
+
label: option.label,
|
|
438
|
+
})) as Parameters<typeof clackSelect<T>>[0]["options"];
|
|
439
|
+
|
|
440
|
+
return guardCancel(
|
|
441
|
+
await clackSelect<T>({
|
|
442
|
+
message,
|
|
443
|
+
options: selectOptions,
|
|
444
|
+
initialValue: initial,
|
|
445
|
+
})
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
449
|
|
|
450
450
|
async function configureDingtalk(prompter: SetupPrompter, cfg: ConfigRoot): Promise<ConfigRoot> {
|
|
451
451
|
section("配置 DingTalk(钉钉)");
|
|
@@ -531,6 +531,7 @@ async function configureWecomApp(prompter: SetupPrompter, cfg: ConfigRoot): Prom
|
|
|
531
531
|
section("配置 WeCom App(自建应用-可接入微信)");
|
|
532
532
|
showGuideLink("wecom-app");
|
|
533
533
|
const existing = getChannelConfig(cfg, "wecom-app");
|
|
534
|
+
const existingAsr = isRecord(existing.asr) ? existing.asr : {};
|
|
534
535
|
|
|
535
536
|
const webhookPath = await prompter.askText({
|
|
536
537
|
label: "Webhook 路径(需与企业微信后台配置一致,默认 /wecom-app)",
|
|
@@ -572,6 +573,38 @@ async function configureWecomApp(prompter: SetupPrompter, cfg: ConfigRoot): Prom
|
|
|
572
573
|
patch.corpId = corpId;
|
|
573
574
|
patch.corpSecret = corpSecret;
|
|
574
575
|
patch.agentId = agentId;
|
|
576
|
+
const asrEnabled = await prompter.askConfirm(
|
|
577
|
+
"启用 ASR(支持入站语音自动转文字)",
|
|
578
|
+
toBoolean(existingAsr.enabled, false)
|
|
579
|
+
);
|
|
580
|
+
const asr: ConfigRecord = {
|
|
581
|
+
enabled: asrEnabled,
|
|
582
|
+
};
|
|
583
|
+
if (asrEnabled) {
|
|
584
|
+
clackNote(
|
|
585
|
+
[
|
|
586
|
+
"ASR 开通方式请查看配置文档:步骤七(可选):开启语音转文本(ASR)",
|
|
587
|
+
"https://github.com/BytePioneer-AI/openclaw-china/blob/main/doc/guides/wecom-app/configuration.md",
|
|
588
|
+
].join("\n"),
|
|
589
|
+
"提示"
|
|
590
|
+
);
|
|
591
|
+
asr.appId = await prompter.askText({
|
|
592
|
+
label: "ASR appId(腾讯云)",
|
|
593
|
+
defaultValue: toTrimmedString(existingAsr.appId),
|
|
594
|
+
required: true,
|
|
595
|
+
});
|
|
596
|
+
asr.secretId = await prompter.askSecret({
|
|
597
|
+
label: "ASR secretId(腾讯云)",
|
|
598
|
+
existingValue: toTrimmedString(existingAsr.secretId),
|
|
599
|
+
required: true,
|
|
600
|
+
});
|
|
601
|
+
asr.secretKey = await prompter.askSecret({
|
|
602
|
+
label: "ASR secretKey(腾讯云)",
|
|
603
|
+
existingValue: toTrimmedString(existingAsr.secretKey),
|
|
604
|
+
required: true,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
patch.asr = asr;
|
|
575
608
|
|
|
576
609
|
return mergeChannelConfig(cfg, "wecom-app", patch);
|
|
577
610
|
}
|
|
@@ -580,6 +613,7 @@ async function configureQQBot(prompter: SetupPrompter, cfg: ConfigRoot): Promise
|
|
|
580
613
|
section("配置 QQBot(QQ 机器人)");
|
|
581
614
|
showGuideLink("qqbot");
|
|
582
615
|
const existing = getChannelConfig(cfg, "qqbot");
|
|
616
|
+
const existingAsr = isRecord(existing.asr) ? existing.asr : {};
|
|
583
617
|
|
|
584
618
|
const appId = await prompter.askText({
|
|
585
619
|
label: "QQBot appId",
|
|
@@ -591,20 +625,46 @@ async function configureQQBot(prompter: SetupPrompter, cfg: ConfigRoot): Promise
|
|
|
591
625
|
existingValue: toTrimmedString(existing.clientSecret),
|
|
592
626
|
required: true,
|
|
593
627
|
});
|
|
628
|
+
clackNote(
|
|
629
|
+
"QQ 的 Markdown 体验很好,但需要先申请开通,详情请查看配置文档。",
|
|
630
|
+
"提示"
|
|
631
|
+
);
|
|
594
632
|
const markdownSupport = await prompter.askConfirm(
|
|
595
633
|
"启用 Markdown 支持",
|
|
596
634
|
toBoolean(existing.markdownSupport, false)
|
|
597
635
|
);
|
|
598
|
-
const
|
|
599
|
-
"
|
|
600
|
-
toBoolean(
|
|
636
|
+
const asrEnabled = await prompter.askConfirm(
|
|
637
|
+
"启用 ASR(支持入站语音自动转文字)",
|
|
638
|
+
toBoolean(existingAsr.enabled, false)
|
|
601
639
|
);
|
|
602
640
|
|
|
641
|
+
const asr: ConfigRecord = {
|
|
642
|
+
enabled: asrEnabled,
|
|
643
|
+
};
|
|
644
|
+
if (asrEnabled) {
|
|
645
|
+
clackNote("ASR 开通方式详情请查看配置文档。", "提示");
|
|
646
|
+
asr.appId = await prompter.askText({
|
|
647
|
+
label: "ASR appId(腾讯云)",
|
|
648
|
+
defaultValue: toTrimmedString(existingAsr.appId),
|
|
649
|
+
required: true,
|
|
650
|
+
});
|
|
651
|
+
asr.secretId = await prompter.askSecret({
|
|
652
|
+
label: "ASR secretId(腾讯云)",
|
|
653
|
+
existingValue: toTrimmedString(existingAsr.secretId),
|
|
654
|
+
required: true,
|
|
655
|
+
});
|
|
656
|
+
asr.secretKey = await prompter.askSecret({
|
|
657
|
+
label: "ASR secretKey(腾讯云)",
|
|
658
|
+
existingValue: toTrimmedString(existingAsr.secretKey),
|
|
659
|
+
required: true,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
603
663
|
return mergeChannelConfig(cfg, "qqbot", {
|
|
604
664
|
appId,
|
|
605
665
|
clientSecret,
|
|
606
666
|
markdownSupport,
|
|
607
|
-
|
|
667
|
+
asr,
|
|
608
668
|
});
|
|
609
669
|
}
|
|
610
670
|
|
|
@@ -629,16 +689,16 @@ async function configureSingleChannel(
|
|
|
629
689
|
}
|
|
630
690
|
}
|
|
631
691
|
|
|
632
|
-
async function runChinaSetup(params: {
|
|
633
|
-
initialConfig: ConfigRoot;
|
|
634
|
-
writeConfig?: WriteConfigLike;
|
|
635
|
-
logger: LoggerLike;
|
|
636
|
-
availableChannels: readonly ChannelId[];
|
|
637
|
-
}): Promise<void> {
|
|
638
|
-
if (!input.isTTY || !output.isTTY) {
|
|
639
|
-
params.logger.error?.("交互式配置需要在 TTY 终端中运行。");
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
692
|
+
async function runChinaSetup(params: {
|
|
693
|
+
initialConfig: ConfigRoot;
|
|
694
|
+
writeConfig?: WriteConfigLike;
|
|
695
|
+
logger: LoggerLike;
|
|
696
|
+
availableChannels: readonly ChannelId[];
|
|
697
|
+
}): Promise<void> {
|
|
698
|
+
if (!input.isTTY || !output.isTTY) {
|
|
699
|
+
params.logger.error?.("交互式配置需要在 TTY 终端中运行。");
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
642
702
|
|
|
643
703
|
const prompter = new SetupPrompter();
|
|
644
704
|
const touched = new Set<ChannelId>();
|
|
@@ -646,37 +706,37 @@ async function runChinaSetup(params: {
|
|
|
646
706
|
|
|
647
707
|
try {
|
|
648
708
|
clackIntro("OpenClaw China 配置向导");
|
|
649
|
-
clackNote(
|
|
650
|
-
[
|
|
651
|
-
"使用方向键选择,按 Enter 确认。",
|
|
652
|
-
`项目仓库:${ANSI_LINK}${PROJECT_REPO}${ANSI_RESET}`,
|
|
653
|
-
].join("\n"),
|
|
654
|
-
"欢迎"
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
if (params.availableChannels.length === 0) {
|
|
658
|
-
params.logger.error?.("未检测到可配置的 China 渠道插件。");
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const channelOptions = params.availableChannels.map((channelId, index) => ({
|
|
663
|
-
key: index === 0 ? "recommended" : "",
|
|
664
|
-
value: channelId,
|
|
665
|
-
label: withConfiguredSuffix(next, channelId),
|
|
666
|
-
}));
|
|
667
|
-
const defaultChannel = channelOptions[0]?.value ?? "save";
|
|
668
|
-
|
|
669
|
-
let continueLoop = true;
|
|
670
|
-
while (continueLoop) {
|
|
671
|
-
const selected = await prompter.askSelect<ChannelId | "save" | "cancel">(
|
|
672
|
-
"请选择要配置的渠道",
|
|
673
|
-
[
|
|
674
|
-
...channelOptions,
|
|
675
|
-
{ key: "", value: "save", label: "保存并退出" },
|
|
676
|
-
{ key: "", value: "cancel", label: "不保存并退出" },
|
|
677
|
-
],
|
|
678
|
-
defaultChannel
|
|
679
|
-
);
|
|
709
|
+
clackNote(
|
|
710
|
+
[
|
|
711
|
+
"使用方向键选择,按 Enter 确认。",
|
|
712
|
+
`项目仓库:${ANSI_LINK}${PROJECT_REPO}${ANSI_RESET}`,
|
|
713
|
+
].join("\n"),
|
|
714
|
+
"欢迎"
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
if (params.availableChannels.length === 0) {
|
|
718
|
+
params.logger.error?.("未检测到可配置的 China 渠道插件。");
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const channelOptions = params.availableChannels.map((channelId, index) => ({
|
|
723
|
+
key: index === 0 ? "recommended" : "",
|
|
724
|
+
value: channelId,
|
|
725
|
+
label: withConfiguredSuffix(next, channelId),
|
|
726
|
+
}));
|
|
727
|
+
const defaultChannel = channelOptions[0]?.value ?? "save";
|
|
728
|
+
|
|
729
|
+
let continueLoop = true;
|
|
730
|
+
while (continueLoop) {
|
|
731
|
+
const selected = await prompter.askSelect<ChannelId | "save" | "cancel">(
|
|
732
|
+
"请选择要配置的渠道",
|
|
733
|
+
[
|
|
734
|
+
...channelOptions,
|
|
735
|
+
{ key: "", value: "save", label: "保存并退出" },
|
|
736
|
+
{ key: "", value: "cancel", label: "不保存并退出" },
|
|
737
|
+
],
|
|
738
|
+
defaultChannel
|
|
739
|
+
);
|
|
680
740
|
|
|
681
741
|
if (selected === "cancel") {
|
|
682
742
|
clackCancel("已取消,未写入任何配置。");
|
|
@@ -722,21 +782,21 @@ async function runChinaSetup(params: {
|
|
|
722
782
|
}
|
|
723
783
|
}
|
|
724
784
|
|
|
725
|
-
export function registerChinaSetupCli(api: ApiLike, opts?: RegisterChinaSetupCliOptions): void {
|
|
726
|
-
const state = getChinaCliState();
|
|
727
|
-
for (const channelId of normalizeChannels(opts?.channels)) {
|
|
728
|
-
state.channels.add(channelId);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
if (state.cliRegistered || typeof api.registerCli !== "function") {
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
state.cliRegistered = true;
|
|
735
|
-
|
|
736
|
-
const writeConfig = resolveWriteConfig(api.runtime);
|
|
737
|
-
const fallbackLogger: LoggerLike = {
|
|
738
|
-
info: (message) => output.write(`${message}\n`),
|
|
739
|
-
warn: (message) => warn(message),
|
|
785
|
+
export function registerChinaSetupCli(api: ApiLike, opts?: RegisterChinaSetupCliOptions): void {
|
|
786
|
+
const state = getChinaCliState();
|
|
787
|
+
for (const channelId of normalizeChannels(opts?.channels)) {
|
|
788
|
+
state.channels.add(channelId);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (state.cliRegistered || typeof api.registerCli !== "function") {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
state.cliRegistered = true;
|
|
795
|
+
|
|
796
|
+
const writeConfig = resolveWriteConfig(api.runtime);
|
|
797
|
+
const fallbackLogger: LoggerLike = {
|
|
798
|
+
info: (message) => output.write(`${message}\n`),
|
|
799
|
+
warn: (message) => warn(message),
|
|
740
800
|
error: (message) => warn(message),
|
|
741
801
|
};
|
|
742
802
|
|
|
@@ -752,30 +812,30 @@ export function registerChinaSetupCli(api: ApiLike, opts?: RegisterChinaSetupCli
|
|
|
752
812
|
|
|
753
813
|
root
|
|
754
814
|
.command("setup")
|
|
755
|
-
.description("中国渠道交互式配置向导")
|
|
756
|
-
.action(async () => {
|
|
757
|
-
const logger = ctx.logger ?? api.logger ?? fallbackLogger;
|
|
758
|
-
const availableChannels = getInstalledChannels(state);
|
|
759
|
-
await runChinaSetup({
|
|
760
|
-
initialConfig: isRecord(ctx.config) ? (ctx.config as ConfigRoot) : {},
|
|
761
|
-
writeConfig,
|
|
762
|
-
logger,
|
|
763
|
-
availableChannels,
|
|
764
|
-
});
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
root.command("about").description("显示项目信息").action(() => {
|
|
768
|
-
const installed = getInstalledChannels(state);
|
|
769
|
-
clackIntro("OpenClaw China 渠道插件");
|
|
770
|
-
clackNote(
|
|
771
|
-
installed.length > 0
|
|
772
|
-
? `当前已安装渠道:${installed.map((channelId) => CHANNEL_DISPLAY_LABELS[channelId]).join("、")}`
|
|
773
|
-
: "OpenClaw China 渠道插件",
|
|
774
|
-
"关于"
|
|
775
|
-
);
|
|
776
|
-
clackOutro(PROJECT_REPO);
|
|
777
|
-
showReadyMessage();
|
|
778
|
-
});
|
|
815
|
+
.description("中国渠道交互式配置向导")
|
|
816
|
+
.action(async () => {
|
|
817
|
+
const logger = ctx.logger ?? api.logger ?? fallbackLogger;
|
|
818
|
+
const availableChannels = getInstalledChannels(state);
|
|
819
|
+
await runChinaSetup({
|
|
820
|
+
initialConfig: isRecord(ctx.config) ? (ctx.config as ConfigRoot) : {},
|
|
821
|
+
writeConfig,
|
|
822
|
+
logger,
|
|
823
|
+
availableChannels,
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
root.command("about").description("显示项目信息").action(() => {
|
|
828
|
+
const installed = getInstalledChannels(state);
|
|
829
|
+
clackIntro("OpenClaw China 渠道插件");
|
|
830
|
+
clackNote(
|
|
831
|
+
installed.length > 0
|
|
832
|
+
? `当前已安装渠道:${installed.map((channelId) => CHANNEL_DISPLAY_LABELS[channelId]).join("、")}`
|
|
833
|
+
: "OpenClaw China 渠道插件",
|
|
834
|
+
"关于"
|
|
835
|
+
);
|
|
836
|
+
clackOutro(PROJECT_REPO);
|
|
837
|
+
showReadyMessage();
|
|
838
|
+
});
|
|
779
839
|
},
|
|
780
840
|
{ commands: ["china"] }
|
|
781
841
|
);
|
package/src/cli/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from "./china-setup.js";
|
|
2
|
-
export * from "./install-hint.js";
|
|
1
|
+
export * from "./china-setup.js";
|
|
2
|
+
export * from "./install-hint.js";
|