botmux 2.23.3 → 2.24.1

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,32 @@
1
+ /**
2
+ * Bot ref resolver for `botmux create-group`. Pure function, no I/O — testable
3
+ * in isolation by passing in mock bot configs + bot-info entries.
4
+ *
5
+ * Resolution order for each ref:
6
+ * 1. Exact `larkAppId` match
7
+ * 2. `botName` from bots-info.json (case-insensitive)
8
+ * 3. `cliId` from bots.json (case-insensitive) — fallback when botName is
9
+ * unknown (bots-info.json gets populated by daemon at startup)
10
+ *
11
+ * Multiple matches by name → take the first in `botConfigs` order (= bots.json
12
+ * traversal order, the user's deployment intent). Same ref repeated → dedup,
13
+ * keeping first occurrence. Unresolvable ref → reported in `invalid` list.
14
+ */
15
+ export interface BotConfigForResolve {
16
+ larkAppId: string;
17
+ cliId: string;
18
+ }
19
+ export interface BotInfoForResolve {
20
+ larkAppId: string;
21
+ botName: string | null;
22
+ }
23
+ export interface ResolvedBots {
24
+ /** Resolved larkAppIds in input order, deduped. First element is creator. */
25
+ larkAppIds: string[];
26
+ /** Refs that couldn't be matched to any bot. */
27
+ invalid: string[];
28
+ /** Warnings about ambiguous name → first match picked. */
29
+ ambiguousWarnings: string[];
30
+ }
31
+ export declare function resolveBotRefs(refs: string[], botConfigs: BotConfigForResolve[], botInfo: BotInfoForResolve[]): ResolvedBots;
32
+ //# sourceMappingURL=create-group-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-group-resolver.d.ts","sourceRoot":"","sources":["../../src/cli/create-group-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,gDAAgD;IAChD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0DAA0D;IAC1D,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EAAE,EACd,UAAU,EAAE,mBAAmB,EAAE,EACjC,OAAO,EAAE,iBAAiB,EAAE,GAC3B,YAAY,CA2Dd"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Bot ref resolver for `botmux create-group`. Pure function, no I/O — testable
3
+ * in isolation by passing in mock bot configs + bot-info entries.
4
+ *
5
+ * Resolution order for each ref:
6
+ * 1. Exact `larkAppId` match
7
+ * 2. `botName` from bots-info.json (case-insensitive)
8
+ * 3. `cliId` from bots.json (case-insensitive) — fallback when botName is
9
+ * unknown (bots-info.json gets populated by daemon at startup)
10
+ *
11
+ * Multiple matches by name → take the first in `botConfigs` order (= bots.json
12
+ * traversal order, the user's deployment intent). Same ref repeated → dedup,
13
+ * keeping first occurrence. Unresolvable ref → reported in `invalid` list.
14
+ */
15
+ export function resolveBotRefs(refs, botConfigs, botInfo) {
16
+ const out = [];
17
+ const seen = new Set();
18
+ const invalid = [];
19
+ const ambiguousWarnings = [];
20
+ for (const ref of refs) {
21
+ const trimmed = ref.trim();
22
+ if (!trimmed)
23
+ continue;
24
+ let matchedAppId;
25
+ let ambiguousLabel;
26
+ // 1. Exact larkAppId
27
+ const byAppId = botConfigs.find(c => c.larkAppId === trimmed);
28
+ if (byAppId) {
29
+ matchedAppId = byAppId.larkAppId;
30
+ }
31
+ else {
32
+ // 2. botName (case-insensitive). bots-info.json is merge-written by
33
+ // multiple daemons and its order is NOT guaranteed to match bots.json.
34
+ // Spec says "重名取 bots.json 中第一个", so we walk botConfigs in
35
+ // deployment order and pick the first whose appId appears in the
36
+ // set of name-matched entries.
37
+ const lower = trimmed.toLowerCase();
38
+ const nameMatchSet = new Set(botInfo.filter(b => b.botName?.toLowerCase() === lower).map(b => b.larkAppId));
39
+ const byNameAll = botConfigs.filter(c => nameMatchSet.has(c.larkAppId));
40
+ if (byNameAll.length > 0) {
41
+ matchedAppId = byNameAll[0].larkAppId;
42
+ if (byNameAll.length > 1)
43
+ ambiguousLabel = `botName "${trimmed}"`;
44
+ }
45
+ else {
46
+ // 3. cliId fallback — relies on botConfigs order which IS bots.json
47
+ // order (loadBotConfigs preserves file traversal order).
48
+ const byCliIdAll = botConfigs.filter(c => c.cliId.toLowerCase() === lower);
49
+ if (byCliIdAll.length > 0) {
50
+ matchedAppId = byCliIdAll[0].larkAppId;
51
+ if (byCliIdAll.length > 1)
52
+ ambiguousLabel = `cliId "${trimmed}"`;
53
+ }
54
+ }
55
+ }
56
+ if (!matchedAppId) {
57
+ invalid.push(trimmed);
58
+ continue;
59
+ }
60
+ if (seen.has(matchedAppId))
61
+ continue;
62
+ seen.add(matchedAppId);
63
+ out.push(matchedAppId);
64
+ if (ambiguousLabel) {
65
+ ambiguousWarnings.push(`${ambiguousLabel} matches multiple bots in bots.json — picked first (${matchedAppId}).`);
66
+ }
67
+ }
68
+ return { larkAppIds: out, invalid, ambiguousWarnings };
69
+ }
70
+ //# sourceMappingURL=create-group-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-group-resolver.js","sourceRoot":"","sources":["../../src/cli/create-group-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAqBH,MAAM,UAAU,cAAc,CAC5B,IAAc,EACd,UAAiC,EACjC,OAA4B;IAE5B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,iBAAiB,GAAa,EAAE,CAAC;IAEvC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3B,IAAI,CAAC,OAAO;YAAE,SAAS;QAEvB,IAAI,YAAgC,CAAC;QACrC,IAAI,cAAkC,CAAC;QAEvC,qBAAqB;QACrB,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,OAAO,CAAC,CAAC;QAC9D,IAAI,OAAO,EAAE,CAAC;YACZ,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,oEAAoE;YACpE,0EAA0E;YAC1E,8DAA8D;YAC9D,oEAAoE;YACpE,kCAAkC;YAClC,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;YACpC,MAAM,YAAY,GAAG,IAAI,GAAG,CAC1B,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAC9E,CAAC;YACF,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;YACxE,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,YAAY,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBACtC,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;oBAAE,cAAc,GAAG,YAAY,OAAO,GAAG,CAAC;YACpE,CAAC;iBAAM,CAAC;gBACN,oEAAoE;gBACpE,4DAA4D;gBAC5D,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,CAAC;gBAC3E,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC1B,YAAY,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;oBACvC,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;wBAAE,cAAc,GAAG,UAAU,OAAO,GAAG,CAAC;gBACnE,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtB,SAAS;QACX,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC;YAAE,SAAS;QACrC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QACvB,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAEvB,IAAI,cAAc,EAAE,CAAC;YACnB,iBAAiB,CAAC,IAAI,CACpB,GAAG,cAAc,uDAAuD,YAAY,IAAI,CACzF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC;AACzD,CAAC"}
package/dist/cli.js CHANGED
@@ -1428,6 +1428,10 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
1428
1428
  bots list 列出当前群聊中的机器人(含 open_id)
1429
1429
  thread messages [--limit N] 拉取当前话题的消息历史 (JSON)
1430
1430
 
1431
+ 新建飞书群:
1432
+ create-group --bot <name> [--bot ...] [--name "群名"]
1433
+ 用指定 bot 起新群;详见 \`botmux create-group --help\`
1434
+
1431
1435
  配置目录: ~/.botmux/
1432
1436
  文档: https://github.com/deepcoldy/botmux
1433
1437
  `);
@@ -2127,6 +2131,147 @@ async function cmdSend(rest) {
2127
2131
  process.exit(1);
2128
2132
  }
2129
2133
  }
2134
+ // ─── Create-group subcommand ─────────────────────────────────────────────────
2135
+ async function cmdCreateGroup(rest) {
2136
+ if (rest.includes('--help') || rest.includes('-h')) {
2137
+ console.log(`
2138
+ botmux create-group — 用一组机器人新建飞书群
2139
+
2140
+ 用法:
2141
+ botmux create-group --bot <name|larkAppId> [--bot ...] [--name "群名"]
2142
+
2143
+ 参数:
2144
+ --bot <ref> 至少一个,可多次。ref 推荐用 bot 显示名(同 botmux send 的 @<name>)或完整 larkAppId;
2145
+ cliId(如 claude-code)仅作 fallback —— 多个 bot 常共用同一个 cliId,重名命中只能取
2146
+ bots.json 中第一个。重名 → 取 bots.json 中第一个匹配,stderr 打 warning。
2147
+ 重复 ref → 自动去重保留首次顺序。
2148
+ --name <群名> 可选;不传则用飞书默认无名群。
2149
+
2150
+ 行为:
2151
+ - 第一个解析到的 bot 作为 creator(决定建群身份 + 初始群主 + open_id app scope)。
2152
+ - 邀请用户 / 转让群主 / @通知 对象都从 creator 的 resolvedAllowedUsers 取首个 open_id(email 自动转换;
2153
+ 转不出来或为空则跳过对应步骤,stderr warning)。
2154
+ - 不依赖 botmux 会话,任何环境都能跑。
2155
+
2156
+ 输出协议(skill 友好):
2157
+ - 成功(即使 transfer/notify 部分失败):stdout 单行 chatId,exit 0;stderr 打人类提示 + applink。
2158
+ - 失败(缺 --bot / 解析失败 / chat.create 抛错):stdout 空,exit 非零;stderr 打错误。
2159
+ `);
2160
+ return;
2161
+ }
2162
+ process.env.SESSION_DATA_DIR ??= resolveDataDir();
2163
+ const botRefs = argValues(rest, '--bot');
2164
+ const name = argValue(rest, '--name');
2165
+ if (botRefs.length === 0) {
2166
+ console.error('用法: botmux create-group --bot <name|larkAppId> [--bot ...] [--name "群名"]');
2167
+ console.error('至少传一个 --bot。');
2168
+ process.exit(1);
2169
+ }
2170
+ // Load bot configs (bots.json order) and bots-info.json (for botName)
2171
+ const { registerBot, loadBotConfigs } = await import('./bot-registry.js');
2172
+ let botConfigs;
2173
+ try {
2174
+ botConfigs = loadBotConfigs().map(c => ({ larkAppId: c.larkAppId, cliId: c.cliId }));
2175
+ }
2176
+ catch (err) {
2177
+ console.error(`加载 bots.json 失败: ${err?.message ?? err}`);
2178
+ process.exit(1);
2179
+ }
2180
+ const dataDir = resolveDataDir();
2181
+ const botInfoPath = join(dataDir, 'bots-info.json');
2182
+ let botInfoEntries = [];
2183
+ try {
2184
+ if (existsSync(botInfoPath))
2185
+ botInfoEntries = JSON.parse(readFileSync(botInfoPath, 'utf-8'));
2186
+ }
2187
+ catch { /* */ }
2188
+ const { resolveBotRefs } = await import('./cli/create-group-resolver.js');
2189
+ const resolved = resolveBotRefs(botRefs, botConfigs, botInfoEntries.map(b => ({ larkAppId: b.larkAppId, botName: b.botName })));
2190
+ for (const w of resolved.ambiguousWarnings)
2191
+ console.error(`⚠️ ${w}`);
2192
+ if (resolved.invalid.length > 0) {
2193
+ console.error(`无法解析的 --bot 引用: ${resolved.invalid.join(', ')}`);
2194
+ console.error('可用 bot:');
2195
+ for (const cfg of botConfigs) {
2196
+ const info = botInfoEntries.find(b => b.larkAppId === cfg.larkAppId);
2197
+ console.error(` - ${info?.botName ?? '(unnamed)'} cliId=${cfg.cliId} ${cfg.larkAppId}`);
2198
+ }
2199
+ process.exit(1);
2200
+ }
2201
+ if (resolved.larkAppIds.length === 0) {
2202
+ console.error('未解析到任何 bot,请检查 --bot 引用。');
2203
+ process.exit(1);
2204
+ }
2205
+ const creatorLarkAppId = resolved.larkAppIds[0];
2206
+ // Register bots so getBotClient works inside service
2207
+ const fullConfigs = loadBotConfigs();
2208
+ const needed = new Set(resolved.larkAppIds);
2209
+ try {
2210
+ for (const cfg of fullConfigs)
2211
+ if (needed.has(cfg.larkAppId))
2212
+ registerBot(cfg);
2213
+ }
2214
+ catch (err) {
2215
+ console.error(`注册 bot 失败: ${err?.message ?? err}`);
2216
+ process.exit(1);
2217
+ }
2218
+ // Derive user_open_id from creator's allowedUsers (creator app scope only).
2219
+ // resolveAllowedUsers converts emails → open_ids via creator's Lark client.
2220
+ const creatorCfg = fullConfigs.find(c => c.larkAppId === creatorLarkAppId);
2221
+ const allowedRaw = creatorCfg?.allowedUsers ?? [];
2222
+ const { resolveAllowedUsers } = await import('./im/lark/client.js');
2223
+ let creatorAllowedOpenIds = [];
2224
+ try {
2225
+ creatorAllowedOpenIds = await resolveAllowedUsers(creatorLarkAppId, allowedRaw);
2226
+ }
2227
+ catch (err) {
2228
+ console.error(`⚠️ 解析 creator allowedUsers 失败: ${err?.message ?? err}(继续创建空群)`);
2229
+ }
2230
+ const targetOpenId = creatorAllowedOpenIds[0];
2231
+ if (!targetOpenId) {
2232
+ console.error('⚠️ creator bot 的 allowedUsers 没有可用 open_id — 将创建仅含 bot 的群(跳过邀请/转让/@通知)。');
2233
+ }
2234
+ const { createGroupWithBots } = await import('./services/group-creator.js');
2235
+ let result;
2236
+ try {
2237
+ result = await createGroupWithBots({
2238
+ creatorLarkAppId,
2239
+ larkAppIds: resolved.larkAppIds,
2240
+ name: name?.trim() || undefined,
2241
+ userOpenIds: targetOpenId ? [targetOpenId] : [],
2242
+ transferOwnerTo: targetOpenId,
2243
+ notifyOwnerOpenId: targetOpenId,
2244
+ });
2245
+ }
2246
+ catch (err) {
2247
+ console.error(`建群失败: ${err?.message ?? err}`);
2248
+ process.exit(1);
2249
+ }
2250
+ // Always stdout chatId on createChat success — even if transfer/notify
2251
+ // partially failed, the chat exists and retrying would create duplicates.
2252
+ process.stdout.write(`${result.chatId}\n`);
2253
+ // Human-readable summary + warnings → stderr.
2254
+ const link = `https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(result.chatId)}`;
2255
+ console.error(`✅ 群已创建:${link}`);
2256
+ if (result.invalidBotIds.length > 0) {
2257
+ console.error(`⚠️ 飞书拒绝邀请的 bot: ${result.invalidBotIds.join(', ')}`);
2258
+ }
2259
+ if (result.invalidUserIds.length > 0) {
2260
+ console.error(`⚠️ 飞书拒绝邀请的 user: ${result.invalidUserIds.join(', ')}`);
2261
+ }
2262
+ if (result.transferError) {
2263
+ console.error(`⚠️ 群主转让失败 (${result.transferError}) — 当前群主仍为 creator bot`);
2264
+ }
2265
+ else if (result.ownerTransferredTo) {
2266
+ console.error(`✅ 群主已转让给 ${result.ownerTransferredTo}`);
2267
+ }
2268
+ if (result.notifyError) {
2269
+ console.error(`⚠️ @通知发送失败: ${result.notifyError}`);
2270
+ }
2271
+ else if (result.notifyMessageId) {
2272
+ console.error(`✅ @通知已发送 (msg ${result.notifyMessageId})`);
2273
+ }
2274
+ }
2130
2275
  // ─── Bots subcommand ─────────────────────────────────────────────────────────
2131
2276
  async function cmdBots(sub, rest) {
2132
2277
  process.env.SESSION_DATA_DIR ??= resolveDataDir();
@@ -2245,6 +2390,9 @@ switch (command) {
2245
2390
  case 'send':
2246
2391
  await cmdSend(process.argv.slice(3));
2247
2392
  break;
2393
+ case 'create-group':
2394
+ await cmdCreateGroup(process.argv.slice(3));
2395
+ break;
2248
2396
  case 'bots':
2249
2397
  await cmdBots(process.argv[3] ?? 'list', process.argv.slice(4));
2250
2398
  break;