@modelzen/feishu-codex-bridge 0.1.8 → 0.2.1-win

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.
Files changed (3) hide show
  1. package/README.md +5 -3
  2. package/dist/cli.js +677 -203
  3. package/package.json +3 -1
package/dist/cli.js CHANGED
@@ -15,7 +15,6 @@ function bridgeVersion() {
15
15
  }
16
16
 
17
17
  // src/cli/commands/doctor.ts
18
- import { execFileSync as execFileSync2 } from "child_process";
19
18
  import { existsSync as existsSync3 } from "fs";
20
19
  import { homedir as homedir2 } from "os";
21
20
  import { join as join4 } from "path";
@@ -269,27 +268,56 @@ async function moveIfExists(src, dest) {
269
268
  }
270
269
 
271
270
  // src/agent/codex-appserver/locate.ts
272
- import { execFileSync } from "child_process";
273
271
  import { existsSync as existsSync2 } from "fs";
274
- import { join as join3 } from "path";
272
+ import { extname, join as join3 } from "path";
273
+
274
+ // src/platform/spawn.ts
275
+ import crossSpawn from "cross-spawn";
276
+ function spawnProcess(command, args = [], options = {}) {
277
+ return crossSpawn(command, [...args], options);
278
+ }
279
+ function spawnProcessSync(command, args = [], options = {}) {
280
+ return crossSpawn.sync(command, [...args], options);
281
+ }
282
+ function mergeProcessEnv(base = process.env, overrides = {}) {
283
+ const out = { ...base };
284
+ for (const [key, value] of Object.entries(overrides)) {
285
+ for (const existing of Object.keys(out)) {
286
+ if (existing.toLowerCase() === key.toLowerCase()) delete out[existing];
287
+ }
288
+ if (value !== void 0) out[key] = value;
289
+ }
290
+ return out;
291
+ }
292
+
293
+ // src/agent/codex-appserver/locate.ts
294
+ var IS_WIN = process.platform === "win32";
275
295
  function resolveCodexBin() {
276
296
  const env = process.env.CODEX_BIN;
277
297
  if (env && existsSync2(env)) return env;
278
298
  const onPath = which("codex");
279
299
  if (onPath) return onPath;
280
- const priv = join3(paths.codexCliBinDir, "codex");
281
- if (existsSync2(priv)) return priv;
300
+ for (const cand of execCandidates(paths.codexCliBinDir, "codex")) {
301
+ if (existsSync2(cand)) return cand;
302
+ }
282
303
  const appBundle = "/Applications/Codex.app/Contents/Resources/codex";
283
304
  if (process.platform === "darwin" && existsSync2(appBundle)) return appBundle;
284
305
  return null;
285
306
  }
307
+ function execCandidates(dir, base) {
308
+ const exact = join3(dir, base);
309
+ if (!IS_WIN || extname(base)) return [exact];
310
+ const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean);
311
+ return [exact, ...exts.map((e) => join3(dir, base + e.toLowerCase()))];
312
+ }
286
313
  function which(cmd) {
287
314
  try {
288
- const out = execFileSync(process.platform === "win32" ? "where" : "which", [cmd], {
315
+ const res = spawnProcessSync(IS_WIN ? "where" : "which", [cmd], {
289
316
  encoding: "utf8",
290
317
  stdio: ["ignore", "pipe", "ignore"]
291
318
  });
292
- const first = out.split("\n").map((l) => l.trim()).find(Boolean);
319
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
320
+ const first = res.stdout.split("\n").map((l) => l.trim()).find(Boolean);
293
321
  return first && existsSync2(first) ? first : null;
294
322
  } catch {
295
323
  return null;
@@ -297,7 +325,9 @@ function which(cmd) {
297
325
  }
298
326
  function codexVersion(bin) {
299
327
  try {
300
- return execFileSync(bin, ["--version"], { encoding: "utf8" }).trim();
328
+ const res = spawnProcessSync(bin, ["--version"], { encoding: "utf8" });
329
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
330
+ return res.stdout.trim();
301
331
  } catch {
302
332
  return null;
303
333
  }
@@ -355,7 +385,9 @@ ${failed === 0 ? "\u5168\u90E8\u901A\u8FC7 \u2713" : `${failed} \u9879\u9700\u59
355
385
  }
356
386
  function tryExec(cmd, args) {
357
387
  try {
358
- return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
388
+ const res = spawnProcessSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
389
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
390
+ return res.stdout.trim();
359
391
  } catch {
360
392
  return null;
361
393
  }
@@ -538,7 +570,7 @@ async function spawnExecProvider(pc, ref) {
538
570
  const timeoutMs = pc.noOutputTimeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
539
571
  const maxOutput = pc.maxOutputBytes ?? DEFAULT_EXEC_MAX_OUTPUT;
540
572
  const providerName = ref.provider ?? DEFAULT_PROVIDER;
541
- return new Promise((resolve6, reject) => {
573
+ return new Promise((resolve7, reject) => {
542
574
  const env = {};
543
575
  if (pc.passEnv) for (const k of pc.passEnv) {
544
576
  const v = process.env[k];
@@ -583,7 +615,7 @@ async function spawnExecProvider(pc, ref) {
583
615
  try {
584
616
  const parsed = JSON.parse(stdout);
585
617
  const value = parsed.values?.[ref.id];
586
- if (typeof value === "string") return resolve6(value);
618
+ if (typeof value === "string") return resolve7(value);
587
619
  const err = parsed.errors?.[ref.id]?.message;
588
620
  reject(new Error(`exec provider did not return secret for ${ref.id}${err ? `: ${err}` : ""}`));
589
621
  } catch (err) {
@@ -679,7 +711,11 @@ var COMMENT_SCOPES = [
679
711
  "docs:document.comment:create",
680
712
  "wiki:wiki:readonly"
681
713
  ];
682
- var GRANT_SCOPES = [...REQUIRED_SCOPES, ...COMMENT_SCOPES];
714
+ var JOIN_GROUP_SCOPES = [
715
+ "im:chat:readonly",
716
+ "im:chat.members:write_only"
717
+ ];
718
+ var GRANT_SCOPES = [...REQUIRED_SCOPES, ...COMMENT_SCOPES, ...JOIN_GROUP_SCOPES];
683
719
  var SCOPE_LABELS = {
684
720
  "im:message.group_at_msg:readonly": "\u63A5\u6536\u7FA4\u91CC @\u673A\u5668\u4EBA \u7684\u6D88\u606F",
685
721
  "im:message.group_msg": "\u63A5\u6536\u7FA4\u5185\u6240\u6709\u6D88\u606F\uFF08\u514D@\uFF09",
@@ -695,6 +731,8 @@ var SCOPE_LABELS = {
695
731
  "im:chat.announcement:write_only": "\u7F16\u8F91\u7FA4\u516C\u544A",
696
732
  "im:chat.top_notice:write_only": "\u7F6E\u9876\u7FA4\u516C\u544A\u6A2A\u5E45",
697
733
  "im:chat.tabs:write_only": "\u6DFB\u52A0\u7FA4\u6807\u7B7E\u9875",
734
+ "im:chat:readonly": "\u8BFB\u53D6\u7FA4\u4FE1\u606F\uFF08\u7FA4\u540D/\u7FA4\u4E3B\uFF0C\u52A0\u5165\u5B58\u91CF\u7FA4\u7528\uFF09",
735
+ "im:chat.members:write_only": "\u7FA4\u6210\u5458\u589E\u51CF\uFF08\u7ED1\u5B9A\u7684\u5B58\u91CF\u7FA4\u89E3\u7ED1\u65F6\u673A\u5668\u4EBA\u9000\u7FA4\uFF09",
698
736
  "cardkit:card:write": "\u4EA4\u4E92\u6309\u94AE\u5361\u7247",
699
737
  "docs:document.comment:read": "\u8BFB\u53D6\u6587\u6863\u8BC4\u8BBA",
700
738
  "docs:document.comment:create": "\u53D1\u8868\u6587\u6863\u8BC4\u8BBA\u56DE\u590D",
@@ -746,8 +784,15 @@ async function validateAppCredentials(appId, appSecret, tenant) {
746
784
  }
747
785
  const token = data.tenant_access_token;
748
786
  const info = await fetchBotInfo(base, token).catch(() => void 0);
749
- const missingScopes = await fetchMissingScopes(base, token).catch(() => void 0);
750
- return { ok: true, botName: info?.bot?.app_name, botOpenId: info?.bot?.open_id, missingScopes };
787
+ const granted = await fetchGrantedScopes(base, token).catch(() => void 0);
788
+ const missing = (list) => granted ? list.filter((s) => !granted.has(s)) : void 0;
789
+ return {
790
+ ok: true,
791
+ botName: info?.bot?.app_name,
792
+ botOpenId: info?.bot?.open_id,
793
+ missingScopes: missing(REQUIRED_SCOPES),
794
+ missingJoinScopes: missing(JOIN_GROUP_SCOPES)
795
+ };
751
796
  }
752
797
  async function fetchBotInfo(base, token) {
753
798
  const resp = await fetch(`${base}/open-apis/bot/v3/info`, {
@@ -756,15 +801,14 @@ async function fetchBotInfo(base, token) {
756
801
  if (!resp.ok) return void 0;
757
802
  return await resp.json();
758
803
  }
759
- async function fetchMissingScopes(base, token) {
804
+ async function fetchGrantedScopes(base, token) {
760
805
  const resp = await fetch(`${base}/open-apis/application/v6/scopes`, {
761
806
  headers: { Authorization: `Bearer ${token}` }
762
807
  });
763
808
  if (!resp.ok) return void 0;
764
809
  const body = await resp.json();
765
810
  if (!body.data?.scopes) return void 0;
766
- const granted = new Set(body.data.scopes.filter((s) => s.grant_status === 1).map((s) => s.scope_name));
767
- return REQUIRED_SCOPES.filter((s) => !granted.has(s));
811
+ return new Set(body.data.scopes.filter((s) => s.grant_status === 1).map((s) => s.scope_name));
768
812
  }
769
813
 
770
814
  // src/utils/open-url.ts
@@ -1071,6 +1115,10 @@ async function confirmReadyForDaemon(result) {
1071
1115
  console.log(" \u2022 \uFF08\u53EF\u9009\uFF09\u60F3\u8981\u300C\u5728\u98DE\u4E66\u6587\u6863\u8BC4\u8BBA\u91CC @\u673A\u5668\u4EBA\u5C31\u81EA\u52A8\u56DE\u590D\u300D\uFF0C\u518D\u52A0\u8FD9\u4E00\u4E2A\u4E8B\u4EF6\uFF1A");
1072
1116
  console.log(" drive.notice.comment_add_v1\uFF08\u4E91\u6587\u6863\u65B0\u589E\u8BC4\u8BBA\uFF09");
1073
1117
  console.log(" \u5B83\u4F9D\u8D56\u300C\u6587\u6863\u8BC4\u8BBA\u300D\u6743\u9650\uFF08docs:document.comment:read / :create\uFF0C\u6388\u6743\u94FE\u63A5\u5DF2\u9884\u52FE\u9009\uFF09\uFF1B\u4E0D\u52A0\u5219\u8BE5\u529F\u80FD\u9759\u9ED8\u5173\u95ED\u3002");
1118
+ console.log(" \u2022 \uFF08\u53EF\u9009\uFF09\u60F3\u8981\u300C\u628A\u6211\u52A0\u8FDB\u5DF2\u6709\u7FA4\u5C31\u80FD\u7ED1\u5B9A\u6210\u9879\u76EE\u300D\uFF0C\u518D\u52A0\u8FD9\u4E24\u4E2A\u4E8B\u4EF6\uFF1A");
1119
+ console.log(" im.chat.member.bot.added_v1\uFF08\u673A\u5668\u4EBA\u88AB\u52A0\u5165\u7FA4 \u2192 \u79C1\u804A\u63A8\u9001\u7ED1\u5B9A\u5361\uFF09");
1120
+ console.log(" im.chat.member.bot.deleted_v1\uFF08\u673A\u5668\u4EBA\u88AB\u79FB\u51FA\u7FA4 \u2192 \u81EA\u52A8\u89E3\u7ED1\u9879\u76EE\uFF09");
1121
+ console.log(" \u5B83\u4EEC\u4F9D\u8D56\u300C\u7FA4\u4FE1\u606F/\u7FA4\u6210\u5458\u300D\u6743\u9650\uFF08im:chat:readonly / im:chat.members:write_only\uFF0C\u5DF2\u9884\u52FE\u9009\uFF09\uFF1B\u4E0D\u52A0\u5219\u8BE5\u529F\u80FD\u9759\u9ED8\u5173\u95ED\u3002");
1074
1122
  console.log(" \u2022 \u5207\u5230\u300C\u56DE\u8C03\u914D\u7F6E\u300D\u6807\u7B7E \u2192 \u300C\u8BA2\u9605\u65B9\u5F0F\u300D\u6539\u300C\u957F\u8FDE\u63A5\u300D\u2192 \u70B9\u300C\u6DFB\u52A0\u56DE\u8C03\u300D\u52FE\u9009\uFF1A");
1075
1123
  console.log(" card.action.trigger\uFF08\u5361\u7247\u56DE\u4F20\u4EA4\u4E92\uFF09");
1076
1124
  console.log(" \u26A0\uFE0F \u5B83\u662F\u300C\u56DE\u8C03\u300D\u4E0D\u662F\u300C\u4E8B\u4EF6\u300D\u2014\u2014\u5728\u4E0A\u9762\u300C\u6DFB\u52A0\u4E8B\u4EF6\u300D\u91CC\u641C\u4E0D\u5230\uFF0C\u5FC5\u987B\u5207\u5230\u300C\u56DE\u8C03\u914D\u7F6E\u300D\u8FD9\u4E2A\u6807\u7B7E\u3002");
@@ -1115,7 +1163,6 @@ ${rule}`);
1115
1163
  import { createLarkChannel, Domain } from "@larksuiteoapi/node-sdk";
1116
1164
 
1117
1165
  // src/agent/codex-appserver/app-server-client.ts
1118
- import { spawn as spawn3 } from "child_process";
1119
1166
  var AsyncQueue = class {
1120
1167
  items = [];
1121
1168
  waiters = [];
@@ -1136,7 +1183,7 @@ var AsyncQueue = class {
1136
1183
  continue;
1137
1184
  }
1138
1185
  if (this.closed) return;
1139
- const next = await new Promise((resolve6) => this.waiters.push(resolve6));
1186
+ const next = await new Promise((resolve7) => this.waiters.push(resolve7));
1140
1187
  if (next.done) return;
1141
1188
  yield next.value;
1142
1189
  }
@@ -1158,9 +1205,9 @@ var AppServerClient = class {
1158
1205
  }
1159
1206
  /** spawn + initialize handshake. Throws if spawn/handshake fails. */
1160
1207
  async connect() {
1161
- const child = spawn3(this.opts.bin, ["app-server", "--listen", "stdio://"], {
1208
+ const child = spawnProcess(this.opts.bin, ["app-server", "--listen", "stdio://"], {
1162
1209
  cwd: this.opts.cwd,
1163
- env: { ...process.env, ...this.opts.env, FEISHU_CODEX_BRIDGE: "1" },
1210
+ env: mergeProcessEnv(process.env, { ...this.opts.env, FEISHU_CODEX_BRIDGE: "1" }),
1164
1211
  stdio: ["pipe", "pipe", "pipe"]
1165
1212
  });
1166
1213
  this.child = child;
@@ -1187,8 +1234,8 @@ var AppServerClient = class {
1187
1234
  const id = ++this.nextId;
1188
1235
  const payload = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}
1189
1236
  `;
1190
- return new Promise((resolve6, reject) => {
1191
- this.pending.set(id, { resolve: resolve6, reject });
1237
+ return new Promise((resolve7, reject) => {
1238
+ this.pending.set(id, { resolve: resolve7, reject });
1192
1239
  this.child.stdin.write(payload, (err) => {
1193
1240
  if (err) {
1194
1241
  this.pending.delete(id);
@@ -1211,15 +1258,36 @@ var AppServerClient = class {
1211
1258
  this.closed = true;
1212
1259
  const child = this.child;
1213
1260
  if (!child || child.exitCode !== null) return;
1261
+ if (process.platform === "win32" && child.pid) {
1262
+ await new Promise((resolve7) => {
1263
+ let settled = false;
1264
+ const done = () => {
1265
+ if (settled) return;
1266
+ settled = true;
1267
+ clearTimeout(t);
1268
+ resolve7();
1269
+ };
1270
+ const t = setTimeout(done, graceMs);
1271
+ child.once("exit", done);
1272
+ spawnProcess("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }).on(
1273
+ "error",
1274
+ () => {
1275
+ child.kill();
1276
+ done();
1277
+ }
1278
+ );
1279
+ });
1280
+ return;
1281
+ }
1214
1282
  child.kill("SIGTERM");
1215
- await new Promise((resolve6) => {
1283
+ await new Promise((resolve7) => {
1216
1284
  const t = setTimeout(() => {
1217
1285
  if (child.exitCode === null) child.kill("SIGKILL");
1218
- resolve6();
1286
+ resolve7();
1219
1287
  }, graceMs);
1220
1288
  child.once("exit", () => {
1221
1289
  clearTimeout(t);
1222
- resolve6();
1290
+ resolve7();
1223
1291
  });
1224
1292
  });
1225
1293
  }
@@ -1352,12 +1420,12 @@ var BRIDGE_DEVELOPER_INSTRUCTIONS = [
1352
1420
  ].join("\n");
1353
1421
  var READ_HISTORY_TIMEOUT_MS = 2e4;
1354
1422
  function withDeadline(p, ms, label) {
1355
- return new Promise((resolve6, reject) => {
1423
+ return new Promise((resolve7, reject) => {
1356
1424
  const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
1357
1425
  p.then(
1358
1426
  (v) => {
1359
1427
  clearTimeout(t);
1360
- resolve6(v);
1428
+ resolve7(v);
1361
1429
  },
1362
1430
  (e) => {
1363
1431
  clearTimeout(t);
@@ -1397,11 +1465,11 @@ var CodexThread = class {
1397
1465
  if (self.model) params.model = self.model;
1398
1466
  if (self.effort) params.effort = self.effort;
1399
1467
  let startError;
1400
- const startFailed = new Promise((resolve6) => {
1468
+ const startFailed = new Promise((resolve7) => {
1401
1469
  self.client.request("turn/start", params).then(void 0, (err) => {
1402
1470
  startError = err instanceof Error ? err : new Error(String(err));
1403
1471
  log.fail("agent", startError, { phase: "turn/start" });
1404
- resolve6("start-failed");
1472
+ resolve7("start-failed");
1405
1473
  });
1406
1474
  });
1407
1475
  const stream2 = self.client.stream()[Symbol.asyncIterator]();
@@ -1707,7 +1775,7 @@ function stampRenderToken(card2) {
1707
1775
  };
1708
1776
  visit(card2);
1709
1777
  }
1710
- async function sendManagedCard(channel, chatId, card2, replyTo, replyInThread = false) {
1778
+ async function sendManagedCard(channel, to, card2, replyTo, replyInThread = false, receiveIdType = "chat_id") {
1711
1779
  stampRenderToken(card2);
1712
1780
  const created = await channel.rawClient.cardkit.v1.card.create({
1713
1781
  data: { type: "card_json", data: JSON.stringify(card2) }
@@ -1726,8 +1794,8 @@ async function sendManagedCard(channel, chatId, card2, replyTo, replyInThread =
1726
1794
  messageId = sent.data?.message_id;
1727
1795
  } else {
1728
1796
  const sent = await channel.rawClient.im.v1.message.create({
1729
- params: { receive_id_type: "chat_id" },
1730
- data: { receive_id: chatId, msg_type: "interactive", content }
1797
+ params: { receive_id_type: receiveIdType },
1798
+ data: { receive_id: to, msg_type: "interactive", content }
1731
1799
  });
1732
1800
  messageId = sent.data?.message_id;
1733
1801
  }
@@ -2183,14 +2251,20 @@ function pickerTime(unixSeconds) {
2183
2251
  const md2 = `${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
2184
2252
  return d.getFullYear() === now.getFullYear() ? `${md2} ${hm}` : `${d.getFullYear()}-${md2} ${hm}`;
2185
2253
  }
2186
- function buildHelpCard(scope) {
2254
+ function talkLine(noMention, tail) {
2255
+ return noMention ? `\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 ${tail}` : `\xB7 **@\u6211 + \u5185\u5BB9** \u2192 ${tail}\uFF08\u672C\u7FA4\u9ED8\u8BA4\u9700 @\uFF1B\`/settings\` \u53EF\u5F00\u542F\u514D@\uFF09`;
2256
+ }
2257
+ function buildHelpCard(scope, noMention = true) {
2187
2258
  const elements = [];
2188
2259
  if (scope === "single") {
2189
2260
  elements.push(
2190
2261
  md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4** \u2014 \u6574\u7FA4\u5C31\u662F\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\u3002"),
2191
2262
  hr(),
2192
2263
  md(
2193
- "\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u4EA4\u7ED9\u6211\u5904\u7406\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09\n\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361"
2264
+ `${talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406")}
2265
+ \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2266
+ \xB7 \`/settings\` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09
2267
+ \xB7 \`/help\` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361`
2194
2268
  )
2195
2269
  );
2196
2270
  } else if (scope === "topic") {
@@ -2198,7 +2272,9 @@ function buildHelpCard(scope) {
2198
2272
  md("\u{1F9F5} **\u8BDD\u9898\u5185** \u2014 \u6BCF\u4E2A\u8BDD\u9898\u662F\u4E00\u4E2A\u72EC\u7ACB\u4F1A\u8BDD\u3002"),
2199
2273
  hr(),
2200
2274
  md(
2201
- "\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/help` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361"
2275
+ `${talkLine(noMention, "\u7EE7\u7EED\u5F53\u524D\u4F1A\u8BDD")}
2276
+ \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2277
+ \xB7 \`/help\` \u2192 \u8FD9\u5F20\u901F\u67E5\u5361`
2202
2278
  ),
2203
2279
  note("\u5F00\u65B0\u8BDD\u9898\uFF1A\u56DE\u5230\u4E3B\u7FA4\u533A @\u6211 + \u5185\u5BB9\u3002")
2204
2280
  );
@@ -2213,7 +2289,7 @@ function buildHelpCard(scope) {
2213
2289
  }
2214
2290
  return card(elements, { header: { title: "\u{1F916} \u53EF\u7528\u547D\u4EE4", template: "blue" }, summary: "\u53EF\u7528\u547D\u4EE4" });
2215
2291
  }
2216
- function buildWelcomeCard(kind, docUrl) {
2292
+ function buildWelcomeCard(kind, docUrl, noMention = true) {
2217
2293
  const elements = [
2218
2294
  md("\u{1F44B} **\u6B22\u8FCE\u4F7F\u7528 Codex Bridge** \u2014 \u672C\u7FA4\u5DF2\u7ED1\u5B9A\u4E00\u4E2A\u9879\u76EE\u76EE\u5F55\uFF0C\u5728\u7FA4\u91CC\u5C31\u80FD\u9A71\u52A8\u672C\u673A Codex \u5E72\u6D3B\u3002"),
2219
2295
  hr()
@@ -2222,7 +2298,10 @@ function buildWelcomeCard(kind, docUrl) {
2222
2298
  elements.push(
2223
2299
  md("\u{1F4AC} **\u5355\u4F1A\u8BDD\u7FA4**\uFF08\u6574\u7FA4\u4E00\u4E2A\u4F1A\u8BDD\uFF0C\u4E0A\u4E0B\u6587\u8FDE\u7EED\uFF09"),
2224
2300
  md(
2225
- "\xB7 \u76F4\u63A5\u53D1\u6D88\u606F\uFF08\u514D@\uFF09\u2192 \u4EA4\u7ED9\u6211\u5904\u7406\n\xB7 `/model` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6\n\xB7 `/settings` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09\n\xB7 `/help` \u2192 \u547D\u4EE4\u901F\u67E5\u5361"
2301
+ `${talkLine(noMention, "\u4EA4\u7ED9\u6211\u5904\u7406")}
2302
+ \xB7 \`/model\` \u2192 \u5207\u6362\u6A21\u578B / \u63A8\u7406\u5F3A\u5EA6
2303
+ \xB7 \`/settings\` \u2192 \u7FA4\u8BBE\u7F6E\uFF08\u514D@ \u5F00\u5173\uFF09
2304
+ \xB7 \`/help\` \u2192 \u547D\u4EE4\u901F\u67E5\u5361`
2226
2305
  )
2227
2306
  );
2228
2307
  } else {
@@ -2761,7 +2840,7 @@ var RunCardStream = class {
2761
2840
 
2762
2841
  // src/card/outbound-images.ts
2763
2842
  import { readFile as readFile5, stat as stat2 } from "fs/promises";
2764
- import { extname, isAbsolute, resolve as resolve2, sep } from "path";
2843
+ import { extname as extname2, isAbsolute, resolve as resolve2, sep } from "path";
2765
2844
  var MAX_IMAGES = 9;
2766
2845
  var MAX_BYTES = 10 * 1024 * 1024;
2767
2846
  var DOWNLOAD_TIMEOUT_MS = 1e4;
@@ -2827,7 +2906,7 @@ async function loadLocal(src, cwd) {
2827
2906
  log.warn("outbound", "image-outside-cwd", { src: src.slice(0, 80) });
2828
2907
  return { cacheKey: `local:${abs}` };
2829
2908
  }
2830
- const ext = extname(abs).slice(1).toLowerCase();
2909
+ const ext = extname2(abs).slice(1).toLowerCase();
2831
2910
  if (!ALLOWED_EXT.has(ext)) {
2832
2911
  log.warn("outbound", "image-ext", { ext, src: src.slice(0, 80) });
2833
2912
  return { cacheKey: `local:${abs}` };
@@ -2895,6 +2974,71 @@ async function uploadBuffer(channel, buffer) {
2895
2974
  return key;
2896
2975
  }
2897
2976
 
2977
+ // src/project/registry.ts
2978
+ import { mkdir as mkdir4, readFile as readFile6, rename as rename4, writeFile as writeFile4 } from "fs/promises";
2979
+ import { dirname as dirname5 } from "path";
2980
+ function defaultNoMention(p) {
2981
+ return !((p.origin ?? "created") === "joined" && (p.kind ?? "multi") === "single");
2982
+ }
2983
+ var FILE_VERSION2 = 1;
2984
+ async function read() {
2985
+ try {
2986
+ const text = await readFile6(paths.projectsFile, "utf8");
2987
+ const parsed = JSON.parse(text);
2988
+ return Array.isArray(parsed.projects) ? parsed.projects : [];
2989
+ } catch (err) {
2990
+ if (err.code === "ENOENT") return [];
2991
+ throw err;
2992
+ }
2993
+ }
2994
+ async function write(projects) {
2995
+ await mkdir4(dirname5(paths.projectsFile), { recursive: true });
2996
+ const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
2997
+ const body = { version: FILE_VERSION2, projects };
2998
+ await writeFile4(tmp, `${JSON.stringify(body, null, 2)}
2999
+ `, "utf8");
3000
+ await rename4(tmp, paths.projectsFile);
3001
+ }
3002
+ async function listProjects() {
3003
+ return read();
3004
+ }
3005
+ async function getProjectByChatId(chatId) {
3006
+ return (await read()).find((p) => p.chatId === chatId);
3007
+ }
3008
+ async function getProjectByName(name) {
3009
+ return (await read()).find((p) => p.name === name);
3010
+ }
3011
+ async function addProject(p) {
3012
+ const projects = await read();
3013
+ if (projects.some((x) => x.name === p.name)) {
3014
+ throw new Error(`\u9879\u76EE\u540D\u300C${p.name}\u300D\u5DF2\u5B58\u5728`);
3015
+ }
3016
+ if (p.chatId) {
3017
+ const bound = projects.find((x) => x.chatId === p.chatId);
3018
+ if (bound) throw new Error(`\u8BE5\u7FA4\u5DF2\u7ED1\u5B9A\u4E3A\u9879\u76EE\u300C${bound.name}\u300D`);
3019
+ }
3020
+ projects.push(p);
3021
+ await write(projects);
3022
+ }
3023
+ async function updateProject(name, patch) {
3024
+ const projects = await read();
3025
+ const p = projects.find((x) => x.name === name);
3026
+ if (!p) return;
3027
+ const target = p;
3028
+ for (const [k, v] of Object.entries(patch)) {
3029
+ if (v !== void 0) target[k] = v;
3030
+ }
3031
+ await write(projects);
3032
+ }
3033
+ async function removeProject(name) {
3034
+ const projects = await read();
3035
+ const idx = projects.findIndex((p) => p.name === name);
3036
+ if (idx === -1) return void 0;
3037
+ const [removed] = projects.splice(idx, 1);
3038
+ await write(projects);
3039
+ return removed;
3040
+ }
3041
+
2898
3042
  // src/card/dm-cards.ts
2899
3043
  function openChatUrl(chatId) {
2900
3044
  return `https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(chatId)}`;
@@ -2904,6 +3048,7 @@ var DM = {
2904
3048
  menu: "dm.menu",
2905
3049
  newProject: "dm.newProject",
2906
3050
  newProjectSubmit: "dm.newProject.submit",
3051
+ joinGroupSubmit: "dm.joinGroup.submit",
2907
3052
  projects: "dm.projects",
2908
3053
  settings: "dm.settings",
2909
3054
  doctor: "dm.doctor",
@@ -3057,6 +3202,27 @@ ${i.missingScopes.map((s) => `\xB7 ${labelScope(s)}`).join("\n")}`),
3057
3202
  actions([linkButton("\u{1F511} \u4E00\u952E\u53BB\u5F00\u901A\u8FD9\u4E9B\u6743\u9650", i.scopeGrantUrl)])
3058
3203
  ];
3059
3204
  }
3205
+ function joinFeatureDiagnosis(i) {
3206
+ const out = [md("**\u52A0\u5165\u5B58\u91CF\u7FA4\uFF08\u53EF\u9009\uFF09**")];
3207
+ if (i.missingJoinScopes === void 0) {
3208
+ out.push(md("- \u6743\u9650\uFF1A\u26A0\uFE0F \u672A\u80FD\u81EA\u52A8\u68C0\u67E5\uFF08\u51ED\u636E\u5931\u6548\u6216\u7F51\u7EDC\u4E0D\u901A\uFF09"), actions([linkButton("\u{1F511} \u53BB\u5F00\u901A", i.joinScopeGrantUrl)]));
3209
+ } else if (i.missingJoinScopes.length === 0) {
3210
+ out.push(md("- \u6743\u9650\uFF1A\u2705 \u5DF2\u5F00\u901A\uFF08`im:chat:readonly` / `im:chat.members:write_only`\uFF09"));
3211
+ } else {
3212
+ out.push(
3213
+ md(`- \u6743\u9650\uFF1A\u274C \u7F3A ${i.missingJoinScopes.length} \u9879 \u2014\u2014 \u5F00\u901A\u540E\u624D\u80FD\u628A\u6211\u52A0\u8FDB\u5DF2\u6709\u7FA4\uFF08\u7ED1\u5B9A / \u9000\u7FA4\uFF09`),
3214
+ note(`\u5F85\u5F00\u901A\uFF1A
3215
+ ${i.missingJoinScopes.map((s) => `\xB7 ${labelScope(s)}`).join("\n")}`),
3216
+ actions([linkButton("\u{1F511} \u4E00\u952E\u5F00\u901A\u8FD9\u4E24\u9879\u6743\u9650", i.joinScopeGrantUrl)])
3217
+ );
3218
+ }
3219
+ out.push(
3220
+ note(
3221
+ "\u26A0\uFE0F \u8FD8\u9700\u5728\u540E\u53F0\u300C\u4E8B\u4EF6\u4E0E\u56DE\u8C03\u300D\u624B\u52A8\u8BA2\u9605 `im.chat.member.bot.added_v1`\uFF08\u88AB\u62C9\u8FDB\u7FA4\u2192\u63A8\u9001\u7ED1\u5B9A\u5361\uFF09\u548C `im.chat.member.bot.deleted_v1`\uFF08\u88AB\u79FB\u51FA\u7FA4\u2192\u81EA\u52A8\u89E3\u7ED1\uFF09\u2014\u2014 \u98DE\u4E66\u65E0\u67E5\u8BE2\u63A5\u53E3\uFF0C\u8FD9\u91CC\u65E0\u6CD5\u81EA\u52A8\u68C0\u6D4B\u3002"
3222
+ )
3223
+ );
3224
+ return out;
3225
+ }
3060
3226
  function codexDiagnosePrompt(i) {
3061
3227
  return [
3062
3228
  "\u6211\u5728\u7528 feishu-codex-bridge\uFF08\u98DE\u4E66 \u2194 \u672C\u5730 Codex \u6865\u63A5\uFF09\u9047\u5230\u95EE\u9898\uFF0C\u8BF7\u5E2E\u6211\u5B9A\u4F4D\u539F\u56E0\u5E76\u7ED9\u51FA\u4FEE\u590D\u6B65\u9AA4\u3002",
@@ -3099,6 +3265,8 @@ function buildDoctorCard(i) {
3099
3265
  ...scopeDiagnosis(i),
3100
3266
  note(`bridge v${i.bridgeVer}\u3000\xB7\u3000Node ${i.node}\u3000\xB7\u3000${i.platform}`),
3101
3267
  hr(),
3268
+ ...joinFeatureDiagnosis(i),
3269
+ hr(),
3102
3270
  md("**\u65E5\u5FD7\u8DEF\u5F84**"),
3103
3271
  note(`\u540E\u53F0\u5B88\u62A4\u8F93\u51FA\uFF1A\`${i.logStdout}\``),
3104
3272
  note(`\u540E\u53F0\u5B88\u62A4\u9519\u8BEF\uFF1A\`${i.logStderr}\``),
@@ -3132,14 +3300,35 @@ function buildNewProjectFormCard(opts = {}) {
3132
3300
  );
3133
3301
  return card(elements, { header: { title: "\u2795 \u65B0\u5EFA\u9879\u76EE", template: "turquoise" } });
3134
3302
  }
3303
+ function buildJoinGroupFormCard(opts) {
3304
+ const elements = [];
3305
+ if (opts.error) elements.push(md(`\u274C **\u7ED1\u5B9A\u5931\u8D25**\uFF1A${opts.error}`));
3306
+ elements.push(
3307
+ md("\u6211\u5DF2\u88AB\u52A0\u5165\u8FD9\u4E2A\u7FA4\u3002\u586B\u4E00\u4E0B\u8981\u7ED1\u5B9A\u7684\u9879\u76EE\u4FE1\u606F\u5373\u53EF\u5F00\u59CB\u7528\u3002"),
3308
+ md("\u9879\u76EE\u540D\u9ED8\u8BA4\u7528\u7FA4\u540D\uFF0C\u53EF\u6539\u3002**\u6587\u4EF6\u5939\u8DEF\u5F84\u7559\u7A7A** = \u81EA\u52A8\u65B0\u5EFA\u7A7A\u767D\u9879\u76EE\uFF1B**\u586B\u7EDD\u5BF9\u8DEF\u5F84** = \u7528\u7535\u8111\u4E0A\u5DF2\u6709\u7684\u6587\u4EF6\u5939\u3002"),
3309
+ form("join_group", [
3310
+ input({ name: "name", label: "\u9879\u76EE\u540D", placeholder: "my-app", value: opts.name, required: true }),
3311
+ input({ name: "cwd", label: "\u6587\u4EF6\u5939\u8DEF\u5F84\uFF08\u9009\u586B\uFF0C\u7559\u7A7A\u81EA\u52A8\u65B0\u5EFA\uFF09", placeholder: "/Users/you/code/my-app", value: opts.cwd }),
3312
+ note("\u9009\u7FA4\u7C7B\u578B(\u76F4\u63A5\u70B9\u5BF9\u5E94\u6309\u94AE\u521B\u5EFA)\uFF1A\u{1F465} \u591A\u8BDD\u9898\u7FA4 = @\u6211\u5F00\u8BDD\u9898\u3001\u6BCF\u8BDD\u9898\u72EC\u7ACB\u4F1A\u8BDD\uFF1B\u{1F4AC} \u5355\u4F1A\u8BDD\u7FA4 = \u6574\u7FA4\u4E00\u4E2A\u4F1A\u8BDD\u3001\u8FDE\u7EED\u4E0A\u4E0B\u6587\uFF08\u9ED8\u8BA4\u4E0D\u514D@\uFF09\u3002"),
3313
+ actions([
3314
+ submitButton("\u{1F465} \u7ED1\u5B9A\xB7\u591A\u8BDD\u9898\u7FA4", { a: DM.joinGroupSubmit, kind: "multi", chatId: opts.chatId }, "primary", "submit_multi"),
3315
+ submitButton("\u{1F4AC} \u7ED1\u5B9A\xB7\u5355\u4F1A\u8BDD\u7FA4", { a: DM.joinGroupSubmit, kind: "single", chatId: opts.chatId }, "primary", "submit_single")
3316
+ ])
3317
+ ])
3318
+ );
3319
+ return card(elements, { header: { title: "\u{1F517} \u7ED1\u5B9A\u5DF2\u6709\u7FA4", template: "turquoise" } });
3320
+ }
3135
3321
  function buildNewProjectDoneCard(p) {
3322
+ const joined = (p.origin ?? "created") === "joined";
3323
+ const verb = joined ? "\u5DF2\u7ED1\u5B9A\u7FA4" : "\u5DF2\u521B\u5EFA\u9879\u76EE";
3324
+ const title = joined ? "\u{1F517} \u7ED1\u5B9A\u5DF2\u6709\u7FA4" : "\u2795 \u65B0\u5EFA\u9879\u76EE";
3136
3325
  const elements = [
3137
- md(`\u2705 \u5DF2\u521B\u5EFA\u9879\u76EE **${p.name}**${p.blank ? " _(\u7A7A\u767D\u9879\u76EE)_" : ""}`),
3326
+ md(`\u2705 ${verb} **${p.name}**${p.blank ? " _(\u7A7A\u767D\u9879\u76EE)_" : ""}`),
3138
3327
  note(`\u{1F4C2} \`${p.cwd}\` \xB7 ${kindLabel(p.kind)}`),
3139
- md(p.chatId ? "\u7FA4\u5DF2\u5EFA\u597D \u{1F449} \u53BB\u9879\u76EE\u7FA4\u91CC **@\u6211** \u5E72\u6D3B\u3002" : "\u53D1\u6211\u4EFB\u610F\u6D88\u606F\u53EF\u518D\u6B21\u6253\u5F00\u7BA1\u7406\u53F0\u3002")
3328
+ md(p.chatId ? "\u{1F449} \u53BB\u7FA4\u91CC **@\u6211** \u5E72\u6D3B\u3002" : "\u53D1\u6211\u4EFB\u610F\u6D88\u606F\u53EF\u518D\u6B21\u6253\u5F00\u7BA1\u7406\u53F0\u3002")
3140
3329
  ];
3141
3330
  if (p.chatId) elements.push(actions([linkButton("\u{1F4AC} \u6253\u5F00\u7FA4\u804A", openChatUrl(p.chatId), "primary")]));
3142
- return card(elements, { header: { title: "\u2795 \u65B0\u5EFA\u9879\u76EE", template: "green" } });
3331
+ return card(elements, { header: { title, template: "green" } });
3143
3332
  }
3144
3333
  function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map()) {
3145
3334
  if (projects.length === 0) {
@@ -3154,7 +3343,7 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3154
3343
  elements.push(note(`\u{1F4C2} \`${p.cwd}\`${p.branch && p.branch !== "\u2014" ? ` \u{1F33F} ${p.branch}` : ""}`));
3155
3344
  elements.push(
3156
3345
  note(
3157
- p.chatId ? `\u{1F4AC} \u7FA4\uFF1A**${p.name}** \xB7 ${kindLabel(p.kind)} \xB7 \u514D@\uFF1A${p.noMention ?? true ? "\u5F00" : "\u5173"}` : "\u26A0\uFE0F \u672A\u7ED1\u5B9A\u7FA4"
3346
+ p.chatId ? `\u{1F4AC} \u7FA4\uFF1A**${p.name}** \xB7 ${kindLabel(p.kind)}${(p.origin ?? "created") === "joined" ? " \xB7 \u{1F517}\u5DF2\u52A0\u5165" : ""} \xB7 \u514D@\uFF1A${p.noMention ?? defaultNoMention(p) ? "\u5F00" : "\u5173"}` : "\u26A0\uFE0F \u672A\u7ED1\u5B9A\u7FA4"
3158
3347
  )
3159
3348
  );
3160
3349
  const sessions = (p.chatId ? sessionsByChat.get(p.chatId) : void 0) ?? [];
@@ -3177,11 +3366,12 @@ function buildProjectListCard(projects, sessionsByChat = /* @__PURE__ */ new Map
3177
3366
  elements.push(actions([button("\u2B05\uFE0F \u83DC\u5355", { a: DM.menu })]));
3178
3367
  return card(elements, { header: { title: "\u{1F4C1} \u9879\u76EE\u5217\u8868", template: "wathet" } });
3179
3368
  }
3180
- function buildRmConfirmCard(name) {
3369
+ function buildRmConfirmCard(name, origin) {
3370
+ const note_ = (origin ?? "created") === "joined" ? "\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u786E\u8BA4\u540E**\u6211\u4F1A\u9000\u51FA\u8BE5\u7FA4**\uFF08\u7FA4\u662F\u4F60\u4EEC\u7684\uFF0C\u4E0D\u4F1A\u89E3\u6563\uFF09\u3002" : "\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C + \u64A4\u9500\u7F6E\u9876\u6A2A\u5E45\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u7FA4\u4E3B\u4F1A\u8F6C\u7ED9\u4F60\uFF0C\u518D\u7531\u4F60\u81EA\u884C\u5728\u98DE\u4E66\u89E3\u6563\u7FA4\u3002";
3181
3371
  return card(
3182
3372
  [
3183
3373
  md(`\u786E\u5B9A\u5220\u9664\u9879\u76EE **${name}**\uFF1F`),
3184
- note("\u4EC5\u89E3\u7ED1\uFF08\u79FB\u9664\u6CE8\u518C + \u64A4\u9500\u7F6E\u9876\u6A2A\u5E45\uFF09\uFF0C**\u4E0D\u5220\u4EE3\u7801\u76EE\u5F55**\u3002\u7FA4\u4E3B\u4F1A\u8F6C\u7ED9\u4F60\uFF0C\u518D\u7531\u4F60\u81EA\u884C\u5728\u98DE\u4E66\u89E3\u6563\u7FA4\u3002"),
3374
+ note(note_),
3185
3375
  actions([
3186
3376
  button("\u2705 \u786E\u8BA4\u5220\u9664", { a: DM.rmDo, n: name }, "danger"),
3187
3377
  button("\u53D6\u6D88", { a: DM.rmCancel })
@@ -3229,7 +3419,7 @@ function buildSettingsCard(cfg) {
3229
3419
  }
3230
3420
  function buildGroupSettingsCard(project) {
3231
3421
  const kind = project.kind ?? "multi";
3232
- const noMention = project.noMention ?? true;
3422
+ const noMention = project.noMention ?? defaultNoMention(project);
3233
3423
  const scopeNote = kind === "single" ? "\u5F00\u542F\u540E\uFF1A\u672C\u7FA4\u6240\u6709\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\u3002" : "\u5F00\u542F\u540E\uFF1A\u8BDD\u9898\u5185\u7684\u6D88\u606F(\u4E0D\u7528 @)\u90FD\u4EA4\u7ED9\u6211\u5904\u7406\uFF1B**\u5F00\u65B0\u8BDD\u9898\u4ECD\u9700 @\u6211**\u3002";
3234
3424
  return card(
3235
3425
  [
@@ -3247,31 +3437,42 @@ function buildGroupSettingsCard(project) {
3247
3437
  }
3248
3438
 
3249
3439
  // src/service/update.ts
3250
- import { execFile, spawn as spawn5 } from "child_process";
3251
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
3252
- import { dirname as dirname6, join as join8, resolve as resolve4 } from "path";
3253
- import { fileURLToPath as fileURLToPath3 } from "url";
3440
+ import { execFile, spawn as spawn4 } from "child_process";
3441
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
3442
+ import { dirname as dirname8, join as join10, resolve as resolve5 } from "path";
3443
+ import { fileURLToPath as fileURLToPath4 } from "url";
3254
3444
  import { promisify } from "util";
3255
3445
 
3256
3446
  // src/service/launchd.ts
3257
- import { spawn as spawn4, spawnSync } from "child_process";
3447
+ import { spawn as spawn3, spawnSync } from "child_process";
3258
3448
  import { existsSync as existsSync4 } from "fs";
3259
- import { appendFile, mkdir as mkdir4, rm as rm2, writeFile as writeFile4 } from "fs/promises";
3449
+ import { mkdir as mkdir6, rm as rm2, writeFile as writeFile5 } from "fs/promises";
3260
3450
  import { homedir as homedir3, userInfo as userInfo2 } from "os";
3261
- import { dirname as dirname5, join as join7, resolve as resolve3 } from "path";
3451
+ import { dirname as dirname6, join as join8, resolve as resolve3 } from "path";
3262
3452
  import { fileURLToPath as fileURLToPath2 } from "url";
3263
- var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
3264
- function launchAgentPlistPath() {
3265
- return join7(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
3266
- }
3453
+
3454
+ // src/service/common.ts
3455
+ import { appendFile, mkdir as mkdir5 } from "fs/promises";
3456
+ import { join as join7 } from "path";
3267
3457
  function serviceStdoutPath() {
3268
3458
  return join7(paths.appDir, "service.log");
3269
3459
  }
3270
3460
  function serviceStderrPath() {
3271
3461
  return join7(paths.appDir, "service.err.log");
3272
3462
  }
3463
+ async function ensureLogFiles() {
3464
+ await mkdir5(paths.appDir, { recursive: true });
3465
+ await appendFile(serviceStdoutPath(), "");
3466
+ await appendFile(serviceStderrPath(), "");
3467
+ }
3468
+
3469
+ // src/service/launchd.ts
3470
+ var LAUNCHD_LABEL = "ai.feishu-codex-bridge.bot";
3471
+ function launchAgentPlistPath() {
3472
+ return join8(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
3473
+ }
3273
3474
  function resolveCliBinPath() {
3274
- const distDir = dirname5(fileURLToPath2(import.meta.url));
3475
+ const distDir = dirname6(fileURLToPath2(import.meta.url));
3275
3476
  return resolve3(distDir, "..", "bin", "feishu-codex-bridge.mjs");
3276
3477
  }
3277
3478
  function escapeXml(value) {
@@ -3312,9 +3513,9 @@ function buildPlist() {
3312
3513
  }
3313
3514
  async function installLaunchd() {
3314
3515
  const plistPath = launchAgentPlistPath();
3315
- await mkdir4(dirname5(plistPath), { recursive: true });
3516
+ await mkdir6(dirname6(plistPath), { recursive: true });
3316
3517
  await ensureLogFiles();
3317
- await writeFile4(plistPath, buildPlist(), "utf8");
3518
+ await writeFile5(plistPath, buildPlist(), "utf8");
3318
3519
  if (isLoaded()) {
3319
3520
  const bootout = runLaunchctl(["bootout", serviceTarget()]);
3320
3521
  if (!bootout.ok) throw launchctlError("launchctl bootout", bootout);
@@ -3350,9 +3551,10 @@ function statusLaunchd() {
3350
3551
  const raw = result.stdout || result.stderr;
3351
3552
  const parsed = parseLaunchdStatus(raw);
3352
3553
  return {
3554
+ platformName: "launchd (macOS)",
3353
3555
  installed: existsSync4(launchAgentPlistPath()),
3354
- loaded: result.ok,
3355
- plistPath: launchAgentPlistPath(),
3556
+ running: result.ok,
3557
+ servicePath: launchAgentPlistPath(),
3356
3558
  stdoutPath: serviceStdoutPath(),
3357
3559
  stderrPath: serviceStderrPath(),
3358
3560
  pid: parsed.pid,
@@ -3364,7 +3566,7 @@ async function tailLaunchdLogs(follow) {
3364
3566
  await ensureLogFiles();
3365
3567
  const args = follow ? ["-f", serviceStdoutPath(), serviceStderrPath()] : ["-n", "100", serviceStdoutPath(), serviceStderrPath()];
3366
3568
  await new Promise((resolvePromise, reject) => {
3367
- const child = spawn4("tail", args, { stdio: "inherit" });
3569
+ const child = spawn3("tail", args, { stdio: "inherit" });
3368
3570
  child.on("error", reject);
3369
3571
  child.on("close", (code) => {
3370
3572
  if (code === 0 || follow && code === null) {
@@ -3395,11 +3597,6 @@ async function waitUntilUnloaded(timeoutMs = 5e3) {
3395
3597
  }
3396
3598
  throw new Error(`launchd service \u672A\u5728 ${timeoutMs}ms \u5185\u5378\u8F7D\u5B8C\u6210`);
3397
3599
  }
3398
- async function ensureLogFiles() {
3399
- await mkdir4(paths.appDir, { recursive: true });
3400
- await appendFile(serviceStdoutPath(), "");
3401
- await appendFile(serviceStderrPath(), "");
3402
- }
3403
3600
  function userTarget() {
3404
3601
  return `gui/${userInfo2().uid}`;
3405
3602
  }
@@ -3420,29 +3617,218 @@ function launchctlError(command, result) {
3420
3617
  return new Error(`${command} \u5931\u8D25\uFF08exit ${result.status ?? "unknown"}\uFF09${output ? `\uFF1A${output}` : ""}`);
3421
3618
  }
3422
3619
 
3620
+ // src/service/schtasks.ts
3621
+ import { spawnSync as spawnSync2 } from "child_process";
3622
+ import { createReadStream, existsSync as existsSync5, statSync } from "fs";
3623
+ import { mkdir as mkdir7, readFile as readFile7, rm as rm3, writeFile as writeFile6 } from "fs/promises";
3624
+ import { dirname as dirname7, join as join9, resolve as resolve4 } from "path";
3625
+ import { fileURLToPath as fileURLToPath3 } from "url";
3626
+ var WINDOWS_TASK_NAME = "feishu-codex-bridge";
3627
+ function launcherCmdPath() {
3628
+ return join9(paths.appDir, "service-launcher.cmd");
3629
+ }
3630
+ function resolveCliBinPath2() {
3631
+ const distDir = dirname7(fileURLToPath3(import.meta.url));
3632
+ return resolve4(distDir, "..", "bin", "feishu-codex-bridge.mjs");
3633
+ }
3634
+ function buildLauncherCmd() {
3635
+ const nodePath = process.execPath;
3636
+ const cliBinPath = resolveCliBinPath2();
3637
+ const pathEnv = process.env.PATH ?? "";
3638
+ return [
3639
+ "@echo off",
3640
+ `set "PATH=${pathEnv}"`,
3641
+ `"${nodePath}" "${cliBinPath}" run >> "${serviceStdoutPath()}" 2>> "${serviceStderrPath()}"`,
3642
+ ""
3643
+ ].join("\r\n");
3644
+ }
3645
+ function runSchtasks(args) {
3646
+ const r = spawnSync2("schtasks", args, { encoding: "utf8" });
3647
+ return {
3648
+ ok: r.status === 0,
3649
+ status: r.status,
3650
+ stdout: r.stdout ?? "",
3651
+ stderr: r.stderr ?? ""
3652
+ };
3653
+ }
3654
+ function schtasksError(command, r) {
3655
+ const out = [r.stderr.trim(), r.stdout.trim()].filter(Boolean).join("\n");
3656
+ return new Error(`${command} \u5931\u8D25\uFF08exit ${r.status ?? "unknown"}\uFF09${out ? `\uFF1A${out}` : ""}`);
3657
+ }
3658
+ async function writeLauncherCmd() {
3659
+ const cmdPath = launcherCmdPath();
3660
+ await mkdir7(dirname7(cmdPath), { recursive: true });
3661
+ await ensureLogFiles();
3662
+ await writeFile6(cmdPath, buildLauncherCmd(), "utf8");
3663
+ }
3664
+ async function installSchtask() {
3665
+ await writeLauncherCmd();
3666
+ const create = runSchtasks([
3667
+ "/Create",
3668
+ "/F",
3669
+ "/SC",
3670
+ "ONLOGON",
3671
+ "/RL",
3672
+ "LIMITED",
3673
+ "/TN",
3674
+ WINDOWS_TASK_NAME,
3675
+ "/TR",
3676
+ `"${launcherCmdPath()}"`
3677
+ ]);
3678
+ if (!create.ok) throw schtasksError("schtasks /Create", create);
3679
+ const run = runSchtasks(["/Run", "/TN", WINDOWS_TASK_NAME]);
3680
+ if (!run.ok) throw schtasksError("schtasks /Run", run);
3681
+ return statusSchtask();
3682
+ }
3683
+ async function uninstallSchtask() {
3684
+ runSchtasks(["/End", "/TN", WINDOWS_TASK_NAME]);
3685
+ const del = runSchtasks(["/Delete", "/F", "/TN", WINDOWS_TASK_NAME]);
3686
+ if (!del.ok && isTaskRegistered()) throw schtasksError("schtasks /Delete", del);
3687
+ if (existsSync5(launcherCmdPath())) await rm3(launcherCmdPath(), { force: true });
3688
+ }
3689
+ async function restartSchtask() {
3690
+ if (!isTaskRegistered()) {
3691
+ throw new Error(`\u8BA1\u5212\u4EFB\u52A1\u672A\u5B89\u88C5\uFF1A${WINDOWS_TASK_NAME}\uFF08\u5148\u8FD0\u884C \`feishu-codex-bridge start\`\uFF09`);
3692
+ }
3693
+ runSchtasks(["/End", "/TN", WINDOWS_TASK_NAME]);
3694
+ await waitUntilStopped();
3695
+ const run = runSchtasks(["/Run", "/TN", WINDOWS_TASK_NAME]);
3696
+ if (!run.ok) throw schtasksError("schtasks /Run", run);
3697
+ return statusSchtask();
3698
+ }
3699
+ function statusSchtask() {
3700
+ const installed = isTaskRegistered();
3701
+ const raw = installed ? describeTask() : "";
3702
+ return {
3703
+ platformName: "Task Scheduler (Windows)",
3704
+ installed,
3705
+ running: installed && /Status:\s+Running/i.test(raw),
3706
+ servicePath: WINDOWS_TASK_NAME,
3707
+ stdoutPath: serviceStdoutPath(),
3708
+ stderrPath: serviceStderrPath(),
3709
+ // `Process ID:` only appears in verbose output while the task is running.
3710
+ pid: raw.match(/Process ID:\s*(\d+)/i)?.[1],
3711
+ // `Last Result: 0` ⇒ last run succeeded. Surface it as the exit code.
3712
+ lastExit: raw.match(/Last Result:\s*(-?\d+)/i)?.[1],
3713
+ raw
3714
+ };
3715
+ }
3716
+ function isTaskRegistered() {
3717
+ const r = spawnSync2("schtasks", ["/Query", "/TN", WINDOWS_TASK_NAME], {
3718
+ stdio: ["ignore", "ignore", "ignore"]
3719
+ });
3720
+ return r.status === 0;
3721
+ }
3722
+ function schtaskRunning() {
3723
+ if (!isTaskRegistered()) return false;
3724
+ return /Status:\s+Running/i.test(describeTask());
3725
+ }
3726
+ function describeTask() {
3727
+ const r = runSchtasks(["/Query", "/V", "/FO", "LIST", "/TN", WINDOWS_TASK_NAME]);
3728
+ return r.stdout || r.stderr || "";
3729
+ }
3730
+ async function waitUntilStopped(timeoutMs = 5e3) {
3731
+ const deadline = Date.now() + timeoutMs;
3732
+ while (Date.now() < deadline) {
3733
+ if (!schtaskRunning()) return true;
3734
+ await new Promise((r) => setTimeout(r, 200));
3735
+ }
3736
+ return false;
3737
+ }
3738
+ async function tailSchtaskLogs(follow) {
3739
+ await ensureLogFiles();
3740
+ const files = [serviceStdoutPath(), serviceStderrPath()];
3741
+ for (const f of files) {
3742
+ const tail = await lastLines(f, 100);
3743
+ if (tail) process.stdout.write(`
3744
+ ===== ${f} =====
3745
+ ${tail}
3746
+ `);
3747
+ }
3748
+ if (!follow) return;
3749
+ const offsets = new Map(files.map((f) => [f, fileSize(f)]));
3750
+ await new Promise((resolvePromise) => {
3751
+ const onSigint = () => {
3752
+ clearInterval(timer);
3753
+ process.off("SIGINT", onSigint);
3754
+ resolvePromise();
3755
+ };
3756
+ process.on("SIGINT", onSigint);
3757
+ const timer = setInterval(() => {
3758
+ for (const f of files) {
3759
+ const size = fileSize(f);
3760
+ const from = offsets.get(f) ?? 0;
3761
+ if (size > from) {
3762
+ offsets.set(f, size);
3763
+ createReadStream(f, { start: from, end: size - 1, encoding: "utf8" }).pipe(process.stdout, {
3764
+ end: false
3765
+ });
3766
+ } else if (size < from) {
3767
+ offsets.set(f, size);
3768
+ }
3769
+ }
3770
+ }, 700);
3771
+ });
3772
+ }
3773
+ function fileSize(file) {
3774
+ try {
3775
+ return statSync(file).size;
3776
+ } catch {
3777
+ return 0;
3778
+ }
3779
+ }
3780
+ async function lastLines(file, n) {
3781
+ try {
3782
+ const text = await readFile7(file, "utf8");
3783
+ const lines = text.split("\n");
3784
+ return lines.slice(-n - 1).join("\n").trimEnd();
3785
+ } catch {
3786
+ return "";
3787
+ }
3788
+ }
3789
+
3423
3790
  // src/service/adapter.ts
3424
3791
  function getServiceAdapter() {
3425
- if (process.platform !== "darwin") {
3426
- throw new Error("service\uFF1A\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\uFF0C\u540E\u7EED\u4F1A\u652F\u6301 Windows/systemd\u3002");
3792
+ if (process.platform === "darwin") {
3793
+ return {
3794
+ install: installLaunchd,
3795
+ uninstall: uninstallLaunchd,
3796
+ status: async () => statusLaunchd(),
3797
+ restart: restartLaunchd,
3798
+ logs: tailLaunchdLogs
3799
+ };
3427
3800
  }
3428
- return {
3429
- install: installLaunchd,
3430
- uninstall: uninstallLaunchd,
3431
- status: async () => statusLaunchd(),
3432
- restart: restartLaunchd,
3433
- logs: tailLaunchdLogs
3434
- };
3801
+ if (process.platform === "win32") {
3802
+ return {
3803
+ install: installSchtask,
3804
+ uninstall: uninstallSchtask,
3805
+ status: async () => statusSchtask(),
3806
+ restart: restartSchtask,
3807
+ logs: tailSchtaskLogs
3808
+ };
3809
+ }
3810
+ throw new Error(
3811
+ "service\uFF1A\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u540E\u53F0\u670D\u52A1\uFF08\u4EC5 macOS launchd / Windows \u8BA1\u5212\u4EFB\u52A1\uFF09\u3002\u8BF7\u7528 `feishu-codex-bridge run` \u524D\u53F0\u8FD0\u884C" + (process.platform === "linux" ? "\uFF1BLinux systemd \u652F\u6301\u540E\u7EED\u63D0\u4F9B\u3002" : "\u3002")
3812
+ );
3813
+ }
3814
+ function isServiceRunning() {
3815
+ try {
3816
+ if (process.platform === "darwin") return isLoaded();
3817
+ if (process.platform === "win32") return schtaskRunning();
3818
+ } catch {
3819
+ }
3820
+ return false;
3435
3821
  }
3436
3822
 
3437
3823
  // src/service/update.ts
3438
3824
  var execFileP = promisify(execFile);
3439
3825
  var NPM = process.platform === "win32" ? "npm.cmd" : "npm";
3440
3826
  function pkgRoot() {
3441
- return resolve4(dirname6(fileURLToPath3(import.meta.url)), "..");
3827
+ return resolve5(dirname8(fileURLToPath4(import.meta.url)), "..");
3442
3828
  }
3443
3829
  function pkgJson() {
3444
3830
  try {
3445
- return JSON.parse(readFileSync2(join8(pkgRoot(), "package.json"), "utf8"));
3831
+ return JSON.parse(readFileSync2(join10(pkgRoot(), "package.json"), "utf8"));
3446
3832
  } catch {
3447
3833
  return {};
3448
3834
  }
@@ -3454,7 +3840,7 @@ function packageName() {
3454
3840
  return pkgJson().name ?? "@modelzen/feishu-codex-bridge";
3455
3841
  }
3456
3842
  function isDevSource() {
3457
- return existsSync5(join8(pkgRoot(), ".git"));
3843
+ return existsSync6(join10(pkgRoot(), ".git"));
3458
3844
  }
3459
3845
  function isNewer(a, b) {
3460
3846
  const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
@@ -3477,7 +3863,7 @@ async function latestVersion() {
3477
3863
  async function installLatest(opts = {}) {
3478
3864
  const target = `${packageName()}@latest`;
3479
3865
  return await new Promise((resolveP) => {
3480
- const child = spawn5(NPM, ["install", "-g", target], {
3866
+ const child = spawn4(NPM, ["install", "-g", target], {
3481
3867
  stdio: opts.inherit ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
3482
3868
  });
3483
3869
  let out = "";
@@ -3493,78 +3879,16 @@ async function installLatest(opts = {}) {
3493
3879
  });
3494
3880
  }
3495
3881
  function daemonRunning() {
3496
- try {
3497
- return statusLaunchd().loaded;
3498
- } catch {
3499
- return false;
3500
- }
3882
+ return isServiceRunning();
3501
3883
  }
3502
3884
  async function restartDaemon() {
3503
3885
  await getServiceAdapter().restart();
3504
3886
  }
3505
3887
 
3506
- // src/project/registry.ts
3507
- import { mkdir as mkdir5, readFile as readFile6, rename as rename4, writeFile as writeFile5 } from "fs/promises";
3508
- import { dirname as dirname7 } from "path";
3509
- var FILE_VERSION2 = 1;
3510
- async function read() {
3511
- try {
3512
- const text = await readFile6(paths.projectsFile, "utf8");
3513
- const parsed = JSON.parse(text);
3514
- return Array.isArray(parsed.projects) ? parsed.projects : [];
3515
- } catch (err) {
3516
- if (err.code === "ENOENT") return [];
3517
- throw err;
3518
- }
3519
- }
3520
- async function write(projects) {
3521
- await mkdir5(dirname7(paths.projectsFile), { recursive: true });
3522
- const tmp = `${paths.projectsFile}.tmp-${process.pid}`;
3523
- const body = { version: FILE_VERSION2, projects };
3524
- await writeFile5(tmp, `${JSON.stringify(body, null, 2)}
3525
- `, "utf8");
3526
- await rename4(tmp, paths.projectsFile);
3527
- }
3528
- async function listProjects() {
3529
- return read();
3530
- }
3531
- async function getProjectByChatId(chatId) {
3532
- return (await read()).find((p) => p.chatId === chatId);
3533
- }
3534
- async function getProjectByName(name) {
3535
- return (await read()).find((p) => p.name === name);
3536
- }
3537
- async function addProject(p) {
3538
- const projects = await read();
3539
- if (projects.some((x) => x.name === p.name)) {
3540
- throw new Error(`\u9879\u76EE\u540D\u300C${p.name}\u300D\u5DF2\u5B58\u5728`);
3541
- }
3542
- projects.push(p);
3543
- await write(projects);
3544
- }
3545
- async function updateProject(name, patch) {
3546
- const projects = await read();
3547
- const p = projects.find((x) => x.name === name);
3548
- if (!p) return;
3549
- const target = p;
3550
- for (const [k, v] of Object.entries(patch)) {
3551
- if (v !== void 0) target[k] = v;
3552
- }
3553
- await write(projects);
3554
- }
3555
- async function removeProject(name) {
3556
- const projects = await read();
3557
- const idx = projects.findIndex((p) => p.name === name);
3558
- if (idx === -1) return void 0;
3559
- const [removed] = projects.splice(idx, 1);
3560
- await write(projects);
3561
- return removed;
3562
- }
3563
-
3564
3888
  // src/project/lifecycle.ts
3565
- import { mkdir as mkdir6 } from "fs/promises";
3566
- import { existsSync as existsSync6 } from "fs";
3567
- import { isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
3889
+ import { mkdir as mkdir8 } from "fs/promises";
3890
+ import { existsSync as existsSync7 } from "fs";
3891
+ import { isAbsolute as isAbsolute2, join as join11, resolve as resolve6 } from "path";
3568
3892
 
3569
3893
  // src/project/git-info.ts
3570
3894
  import { execFile as execFile2 } from "child_process";
@@ -3665,21 +3989,23 @@ var HELP_DOC_URL = "https://my.feishu.cn/wiki/PZ23wGr7JiKK5RkIG4rcZXzGn5g";
3665
3989
  async function onboardGroup(channel, project) {
3666
3990
  const kind = project.kind ?? "multi";
3667
3991
  const chatId = project.chatId;
3992
+ const decorate = (project.origin ?? "created") !== "joined";
3668
3993
  try {
3669
- const content = JSON.stringify(buildWelcomeCard(kind, HELP_DOC_URL || void 0));
3994
+ const noMention = project.noMention ?? defaultNoMention(project);
3995
+ const content = JSON.stringify(buildWelcomeCard(kind, HELP_DOC_URL || void 0, noMention));
3670
3996
  const sent = await channel.rawClient.im.v1.message.create({
3671
3997
  params: { receive_id_type: "chat_id" },
3672
3998
  data: { receive_id: chatId, msg_type: "interactive", content }
3673
3999
  });
3674
4000
  const messageId = sent.data?.message_id;
3675
- if (messageId) {
4001
+ if (messageId && decorate) {
3676
4002
  await channel.rawClient.im.v1.pin.create({ data: { message_id: messageId } });
3677
4003
  log.info("project", "onboard-pin", { name: project.name });
3678
4004
  }
3679
4005
  } catch (err) {
3680
4006
  log.fail("project", err, { phase: "onboard-welcome" });
3681
4007
  }
3682
- if (HELP_DOC_URL) {
4008
+ if (decorate && HELP_DOC_URL) {
3683
4009
  try {
3684
4010
  await channel.rawClient.im.v1.chatTab.create({
3685
4011
  path: { chat_id: chatId },
@@ -3695,21 +4021,21 @@ async function onboardGroup(channel, project) {
3695
4021
  }
3696
4022
 
3697
4023
  // src/project/lifecycle.ts
4024
+ async function resolveCwd(name, existingPath) {
4025
+ if (existingPath) {
4026
+ const cwd2 = isAbsolute2(existingPath) ? existingPath : resolve6(existingPath);
4027
+ if (!existsSync7(cwd2)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd2}`);
4028
+ return { cwd: cwd2, blank: false };
4029
+ }
4030
+ const cwd = join11(paths.projectsRootDir, name);
4031
+ await mkdir8(cwd, { recursive: true });
4032
+ return { cwd, blank: true };
4033
+ }
3698
4034
  async function createProject(channel, input2) {
3699
4035
  const name = input2.name.trim();
3700
4036
  if (!name) throw new Error("\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A");
3701
4037
  if (await getProjectByName(name)) throw new Error(`\u9879\u76EE\u540D\u300C${name}\u300D\u5DF2\u5B58\u5728\uFF0C\u6362\u4E2A\u540D\u6216\u7528 /projects \u770B\u5DF2\u6709\u7684`);
3702
- let cwd;
3703
- let blank;
3704
- if (input2.existingPath) {
3705
- cwd = isAbsolute2(input2.existingPath) ? input2.existingPath : resolve5(input2.existingPath);
3706
- if (!existsSync6(cwd)) throw new Error(`\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF1A${cwd}`);
3707
- blank = false;
3708
- } else {
3709
- cwd = join9(paths.projectsRootDir, name);
3710
- await mkdir6(cwd, { recursive: true });
3711
- blank = true;
3712
- }
4038
+ const { cwd, blank } = await resolveCwd(name, input2.existingPath);
3713
4039
  const res = await channel.rawClient.im.v1.chat.create({
3714
4040
  params: { user_id_type: "open_id" },
3715
4041
  data: { name, user_id_list: [input2.ownerOpenId] }
@@ -3721,13 +4047,35 @@ async function createProject(channel, input2) {
3721
4047
  params: { member_id_type: "open_id" },
3722
4048
  data: { manager_ids: [input2.ownerOpenId] }
3723
4049
  }).catch((err) => log.fail("project", err, { phase: "add-manager" }));
3724
- const project = { name, chatId, cwd, blank, createdAt: Date.now(), kind: input2.kind ?? "multi" };
4050
+ const project = { name, chatId, cwd, blank, createdAt: Date.now(), kind: input2.kind ?? "multi", origin: "created" };
3725
4051
  await addProject(project);
3726
4052
  log.info("project", "create", { name, chatId, cwd, blank });
3727
4053
  await setAnnouncement(channel, project).catch((err) => log.fail("project", err, { phase: "announcement" }));
3728
4054
  await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard" }));
3729
4055
  return project;
3730
4056
  }
4057
+ async function joinExistingGroup(channel, input2) {
4058
+ const name = input2.name.trim();
4059
+ if (!name) throw new Error("\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A");
4060
+ if (await getProjectByName(name)) throw new Error(`\u9879\u76EE\u540D\u300C${name}\u300D\u5DF2\u5B58\u5728\uFF0C\u6362\u4E2A\u540D\u6216\u7528 /projects \u770B\u5DF2\u6709\u7684`);
4061
+ const bound = await getProjectByChatId(input2.chatId);
4062
+ if (bound) throw new Error(`\u8BE5\u7FA4\u5DF2\u7ED1\u5B9A\u4E3A\u9879\u76EE\u300C${bound.name}\u300D`);
4063
+ const { cwd, blank } = await resolveCwd(name, input2.existingPath);
4064
+ const project = {
4065
+ name,
4066
+ chatId: input2.chatId,
4067
+ cwd,
4068
+ blank,
4069
+ createdAt: Date.now(),
4070
+ kind: input2.kind ?? "multi",
4071
+ origin: "joined",
4072
+ addedBy: input2.addedBy
4073
+ };
4074
+ await addProject(project);
4075
+ log.info("project", "join", { name, chatId: input2.chatId, cwd, blank, kind: project.kind });
4076
+ await onboardGroup(channel, project).catch((err) => log.fail("project", err, { phase: "onboard-join" }));
4077
+ return project;
4078
+ }
3731
4079
 
3732
4080
  // src/project/group-ops.ts
3733
4081
  async function transferOwnership(channel, chatId, toOpenId) {
@@ -3738,14 +4086,21 @@ async function transferOwnership(channel, chatId, toOpenId) {
3738
4086
  });
3739
4087
  log.info("project", "owner-transfer", { chatId: chatId.slice(-6), to: toOpenId.slice(-6) });
3740
4088
  }
4089
+ async function leaveChat(channel, chatId) {
4090
+ await channel.rawClient.request({
4091
+ method: "PATCH",
4092
+ url: `/open-apis/im/v1/chats/${encodeURIComponent(chatId)}/members/me_leave`
4093
+ });
4094
+ log.info("project", "leave-chat", { chatId: chatId.slice(-6) });
4095
+ }
3741
4096
 
3742
4097
  // src/bot/session-store.ts
3743
- import { mkdir as mkdir7, readFile as readFile7, rename as rename5, writeFile as writeFile6 } from "fs/promises";
3744
- import { dirname as dirname8 } from "path";
4098
+ import { mkdir as mkdir9, readFile as readFile8, rename as rename5, writeFile as writeFile7 } from "fs/promises";
4099
+ import { dirname as dirname9 } from "path";
3745
4100
  var FILE_VERSION3 = 1;
3746
4101
  async function read2() {
3747
4102
  try {
3748
- const text = await readFile7(paths.sessionsFile, "utf8");
4103
+ const text = await readFile8(paths.sessionsFile, "utf8");
3749
4104
  const parsed = JSON.parse(text);
3750
4105
  return Array.isArray(parsed.sessions) ? parsed.sessions : [];
3751
4106
  } catch (err) {
@@ -3754,10 +4109,10 @@ async function read2() {
3754
4109
  }
3755
4110
  }
3756
4111
  async function write2(sessions) {
3757
- await mkdir7(dirname8(paths.sessionsFile), { recursive: true });
4112
+ await mkdir9(dirname9(paths.sessionsFile), { recursive: true });
3758
4113
  const tmp = `${paths.sessionsFile}.tmp-${process.pid}`;
3759
4114
  const body = { version: FILE_VERSION3, sessions };
3760
- await writeFile6(tmp, `${JSON.stringify(body, null, 2)}
4115
+ await writeFile7(tmp, `${JSON.stringify(body, null, 2)}
3761
4116
  `, "utf8");
3762
4117
  await rename5(tmp, paths.sessionsFile);
3763
4118
  }
@@ -3801,8 +4156,8 @@ async function handleDmConsole(channel, cfg, msg) {
3801
4156
  }
3802
4157
 
3803
4158
  // src/bot/media.ts
3804
- import { mkdir as mkdir8, readdir as readdir2, rm as rm3, stat as stat3 } from "fs/promises";
3805
- import { join as join10 } from "path";
4159
+ import { mkdir as mkdir10, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
4160
+ import { join as join12 } from "path";
3806
4161
  var MAX_IMAGES2 = 9;
3807
4162
  var MEDIA_TTL_MS = 60 * 6e4;
3808
4163
  var EXT_BY_CONTENT_TYPE = {
@@ -3831,7 +4186,7 @@ async function collectInboundImages(channel, msg) {
3831
4186
  if (refs.length === 0) return [];
3832
4187
  await pruneOldMedia();
3833
4188
  try {
3834
- await mkdir8(paths.mediaDir, { recursive: true });
4189
+ await mkdir10(paths.mediaDir, { recursive: true });
3835
4190
  } catch {
3836
4191
  }
3837
4192
  const out = [];
@@ -3907,7 +4262,7 @@ async function downloadOne(channel, ref, index) {
3907
4262
  params: { type: "image" }
3908
4263
  });
3909
4264
  const ext = extFromHeaders(res.headers);
3910
- const file = join10(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
4265
+ const file = join12(paths.mediaDir, `${safeName(ref.fileKey)}-${index}.${ext}`);
3911
4266
  await res.writeFile(file);
3912
4267
  return file;
3913
4268
  } catch (err) {
@@ -3941,10 +4296,10 @@ async function pruneOldMedia() {
3941
4296
  }
3942
4297
  const cutoff = Date.now() - MEDIA_TTL_MS;
3943
4298
  for (const name of entries) {
3944
- const file = join10(paths.mediaDir, name);
4299
+ const file = join12(paths.mediaDir, name);
3945
4300
  try {
3946
4301
  const st = await stat3(file);
3947
- if (st.mtimeMs < cutoff) await rm3(file, { force: true });
4302
+ if (st.mtimeMs < cutoff) await rm4(file, { force: true });
3948
4303
  } catch {
3949
4304
  }
3950
4305
  }
@@ -4268,11 +4623,22 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4268
4623
  log.info("intake", "reject", { reason: "not_allowed", chatId: msg.chatId.slice(-6) });
4269
4624
  return;
4270
4625
  }
4626
+ if (!project) {
4627
+ log.info("intake", "unbound-group", { chatId: msg.chatId.slice(-6), atBot: msg.mentionedBot });
4628
+ if (msg.mentionedBot) {
4629
+ await channel.send(
4630
+ msg.chatId,
4631
+ { markdown: "\u672C\u7FA4\u8FD8\u6CA1\u7ED1\u5B9A\u4E3A\u9879\u76EE\u3002\u8BF7**\u628A\u6211\u62C9\u8FDB\u7FA4\u7684\u7BA1\u7406\u5458**\u5728\u4E0E\u6211\u7684\u79C1\u804A\u91CC\u5B8C\u6210\u7ED1\u5B9A\u540E\u518D @\u6211\u3002" },
4632
+ { replyTo: msg.messageId }
4633
+ ).catch(() => void 0);
4634
+ }
4635
+ return;
4636
+ }
4271
4637
  const text = msg.content.trim();
4272
4638
  const cmd = parseCommand(text);
4273
4639
  if ((project?.kind ?? "multi") === "single") {
4274
4640
  if (cmd === "help") {
4275
- await postHelpCard(msg, "single");
4641
+ await postHelpCard(msg, "single", false, project);
4276
4642
  return;
4277
4643
  }
4278
4644
  if (cmd === "settings") {
@@ -4288,7 +4654,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4288
4654
  }
4289
4655
  if (msg.threadId) {
4290
4656
  if (cmd === "help") {
4291
- await postHelpCard(msg, "topic", true);
4657
+ await postHelpCard(msg, "topic", true, project);
4292
4658
  return;
4293
4659
  }
4294
4660
  if (cmd === "model") {
@@ -4299,7 +4665,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4299
4665
  return;
4300
4666
  }
4301
4667
  if (cmd === "help") {
4302
- await postHelpCard(msg, "main");
4668
+ await postHelpCard(msg, "main", false, project);
4303
4669
  return;
4304
4670
  }
4305
4671
  if (cmd === "resume") {
@@ -4322,7 +4688,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4322
4688
  return name === "resume" || name === "model" || name === "settings" || name === "help" ? name : null;
4323
4689
  }
4324
4690
  function shouldRespondWithoutMention(project, msg) {
4325
- if (!(project.noMention ?? true)) return false;
4691
+ if (!(project.noMention ?? defaultNoMention(project))) return false;
4326
4692
  if (msg.mentionAll || msg.mentions.some((m) => !m.isBot)) return false;
4327
4693
  if ((project.kind ?? "multi") === "single") return true;
4328
4694
  return Boolean(msg.threadId) || parseCommand(msg.content.trim()) !== null;
@@ -4519,9 +4885,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4519
4885
  log.info("card", "model", { threadId: sessionKey, model: state.model, effort: state.effort });
4520
4886
  });
4521
4887
  }
4522
- async function postHelpCard(msg, scope, inThread = false) {
4888
+ async function postHelpCard(msg, scope, inThread = false, project) {
4889
+ const noMention = project ? project.noMention ?? defaultNoMention(project) : true;
4523
4890
  await withTrace({ chatId: msg.chatId, msgId: msg.messageId }, async () => {
4524
- await sendManagedCard(channel, msg.chatId, buildHelpCard(scope), msg.messageId, inThread).catch(
4891
+ await sendManagedCard(channel, msg.chatId, buildHelpCard(scope, noMention), msg.messageId, inThread).catch(
4525
4892
  (err) => log.fail("card", err, { cmd: "help", scope })
4526
4893
  );
4527
4894
  log.info("card", "help", { scope });
@@ -4659,6 +5026,32 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4659
5026
  (e) => log.fail("console", e, { phase: "new-project-result" })
4660
5027
  );
4661
5028
  })();
5029
+ }).on(DM.joinGroupSubmit, ({ evt, formValue, value }) => {
5030
+ const op = evt.operator?.openId;
5031
+ if (!dmAdmin(op)) return;
5032
+ const name = String(formValue?.name ?? "").trim();
5033
+ const cwdIn = String(formValue?.cwd ?? "").trim();
5034
+ const chatId = typeof value.chatId === "string" ? value.chatId : "";
5035
+ const kind = value.kind === "single" ? "single" : "multi";
5036
+ void (async () => {
5037
+ let result;
5038
+ if (!chatId)
5039
+ result = buildJoinGroupFormCard({ chatId: "", name, cwd: cwdIn, error: "\u7F3A\u5C11\u7FA4\u6807\u8BC6\uFF0C\u8BF7\u91CD\u65B0\u4ECE\u8FDB\u7FA4\u901A\u77E5\u91CC\u6253\u5F00\u7ED1\u5B9A\u5361" });
5040
+ else if (!name) result = buildJoinGroupFormCard({ chatId, cwd: cwdIn, error: "\u9879\u76EE\u540D\u4E0D\u80FD\u4E3A\u7A7A" });
5041
+ else if (!op) result = buildJoinGroupFormCard({ chatId, name, cwd: cwdIn, error: "\u65E0\u6CD5\u8BC6\u522B\u64CD\u4F5C\u8005\u8EAB\u4EFD" });
5042
+ else {
5043
+ try {
5044
+ const p = await joinExistingGroup(channel, { name, chatId, addedBy: op, existingPath: cwdIn || void 0, kind });
5045
+ log.info("console", "join-group", { name: p.name, blank: p.blank });
5046
+ result = buildNewProjectDoneCard(p);
5047
+ } catch (err) {
5048
+ result = buildJoinGroupFormCard({ chatId, name, cwd: cwdIn, error: err instanceof Error ? err.message : String(err) });
5049
+ }
5050
+ }
5051
+ await sendManagedCard(channel, evt.chatId, result).catch(
5052
+ (e) => log.fail("console", e, { phase: "join-group-result" })
5053
+ );
5054
+ })();
4662
5055
  }).on(DM.projects, ({ evt }) => {
4663
5056
  if (!dmAdmin(evt.operator?.openId)) return;
4664
5057
  patch(evt, renderProjectList);
@@ -4671,6 +5064,7 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4671
5064
  const secret = await getSecret(secretKeyForApp(app.id)).catch(() => void 0);
4672
5065
  const scopeCheck = secret ? await validateAppCredentials(app.id, secret, app.tenant).catch(() => void 0) : void 0;
4673
5066
  const missingScopes = scopeCheck?.missingScopes;
5067
+ const missingJoinScopes = scopeCheck?.missingJoinScopes;
4674
5068
  const info = {
4675
5069
  codexOk: await backend.isAvailable().catch(() => false),
4676
5070
  codexVer: codexBin ? codexVersion(codexBin) : null,
@@ -4687,7 +5081,10 @@ function createOrchestrator(channel, cfg, fallbackCwd) {
4687
5081
  app.id,
4688
5082
  app.tenant,
4689
5083
  missingScopes && missingScopes.length ? missingScopes : void 0
4690
- )
5084
+ ),
5085
+ missingJoinScopes,
5086
+ // 「加入存量群」按钮恒预选这两项 opt-in scope(它们不在必需清单里)。
5087
+ joinScopeGrantUrl: buildScopeGrantUrl(app.id, app.tenant, JOIN_GROUP_SCOPES)
4691
5088
  };
4692
5089
  await sendManagedCard(channel, evt.chatId, buildDoctorCard(info), evt.messageId).catch(
4693
5090
  (err) => log.fail("console", err, { cmd: "doctor" })
@@ -4748,7 +5145,8 @@ SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8B
4748
5145
  }).on(DM.rmConfirm, async ({ evt, value }) => {
4749
5146
  const name = typeof value.n === "string" ? value.n : void 0;
4750
5147
  if (!dmAdmin(evt.operator?.openId) || !name) return;
4751
- await patch(evt, buildRmConfirmCard(name));
5148
+ const proj = (await listProjects()).find((p) => p.name === name);
5149
+ await patch(evt, buildRmConfirmCard(name, proj?.origin));
4752
5150
  }).on(DM.rmCancel, ({ evt }) => {
4753
5151
  if (!dmAdmin(evt.operator?.openId)) return;
4754
5152
  patch(evt, renderProjectList);
@@ -4758,15 +5156,25 @@ SDK \u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF1B\u82E5\u957F\u671F\u65AD\u5F00\uFF0C\u8B
4758
5156
  if (!dmAdmin(op) || !name) return;
4759
5157
  patch(evt, async () => {
4760
5158
  const removed = await removeProject(name);
4761
- let transferred = false;
4762
- if (removed?.chatId && op) {
4763
- transferred = await transferOwnership(channel, removed.chatId, op).then(() => true).catch((err) => {
4764
- log.fail("console", err, { phase: "owner-transfer" });
5159
+ let tail;
5160
+ if (removed && (removed.origin ?? "created") === "joined") {
5161
+ const left = removed.chatId ? await leaveChat(channel, removed.chatId).then(() => true).catch((err) => {
5162
+ log.fail("console", err, { phase: "leave-chat" });
4765
5163
  return false;
4766
- });
5164
+ }) : false;
5165
+ log.info("console", "rm", { name, origin: "joined", left });
5166
+ tail = left ? "\u6211\u5DF2\u9000\u51FA\u8BE5\u7FA4\uFF08\u7FA4\u662F\u4F60\u4EEC\u7684\uFF0C\u4E0D\u4F1A\u89E3\u6563\uFF09\u3002" : "\u26A0\uFE0F \u6211\u9000\u7FA4\u5931\u8D25\uFF08\u53EF\u80FD\u6743\u9650\u4E0D\u8DB3\uFF09\uFF0C\u53EF\u5728\u7FA4\u91CC\u624B\u52A8\u628A\u6211\u79FB\u9664\u3002";
5167
+ } else {
5168
+ let transferred = false;
5169
+ if (removed?.chatId && op) {
5170
+ transferred = await transferOwnership(channel, removed.chatId, op).then(() => true).catch((err) => {
5171
+ log.fail("console", err, { phase: "owner-transfer" });
5172
+ return false;
5173
+ });
5174
+ }
5175
+ log.info("console", "rm", { name, origin: "created", transferred });
5176
+ tail = transferred ? "\u7FA4\u4E3B\u5DF2\u8F6C\u7ED9\u4F60 \u2192 \u8BF7\u5728\u98DE\u4E66\u91CC**\u81EA\u884C\u89E3\u6563\u8BE5\u7FA4**\uFF08\u673A\u5668\u4EBA\u4E0D\u4E3B\u52A8\u89E3\u6563\uFF09\u3002" : "\u26A0\uFE0F \u7FA4\u4E3B\u8F6C\u8BA9\u5931\u8D25\uFF08\u53EF\u80FD bot \u975E\u7FA4\u4E3B\uFF09\uFF0C\u8BF7\u7528\u300C\u{1F6AA} \u7FA4\u7BA1\u7406\u300D\u624B\u52A8\u8F6C\u8BA9\u540E\u89E3\u6563\u3002";
4767
5177
  }
4768
- log.info("console", "rm", { name, transferred });
4769
- const tail = transferred ? "\u7FA4\u4E3B\u5DF2\u8F6C\u7ED9\u4F60 \u2192 \u8BF7\u5728\u98DE\u4E66\u91CC**\u81EA\u884C\u89E3\u6563\u8BE5\u7FA4**\uFF08\u673A\u5668\u4EBA\u4E0D\u4E3B\u52A8\u89E3\u6563\uFF09\u3002" : "\u26A0\uFE0F \u7FA4\u4E3B\u8F6C\u8BA9\u5931\u8D25\uFF08\u53EF\u80FD bot \u975E\u7FA4\u4E3B\uFF09\uFF0C\u8BF7\u7528\u300C\u{1F6AA} \u7FA4\u7BA1\u7406\u300D\u624B\u52A8\u8F6C\u8BA9\u540E\u89E3\u6563\u3002";
4770
5178
  await channel.send(evt.chatId, { markdown: `\u2705 \u5DF2\u5220\u9664\u9879\u76EE\u300C${name}\u300D\uFF08\u89E3\u7ED1\uFF0C\u672A\u5220\u4EE3\u7801\u76EE\u5F55\uFF09\u3002
4771
5179
  ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
4772
5180
  return renderProjectList();
@@ -5102,13 +5510,57 @@ ${tail}` }, { replyTo: evt.messageId }).catch(() => void 0);
5102
5510
  });
5103
5511
  return fresh;
5104
5512
  }
5513
+ async function onBotAddedToChat(evt) {
5514
+ await withTrace({ chatId: evt.chatId }, async () => {
5515
+ const op = evt.operator?.openId;
5516
+ if (await getProjectByChatId(evt.chatId)) {
5517
+ log.info("intake", "bot-added-bound", { chatId: evt.chatId.slice(-6) });
5518
+ return;
5519
+ }
5520
+ if (!op || !isAdmin(cfg, op)) {
5521
+ log.info("intake", "bot-added-nonadmin", { chatId: evt.chatId.slice(-6), op: op?.slice(-6) });
5522
+ return;
5523
+ }
5524
+ const info = await channel.getChatInfo(evt.chatId).catch((err) => {
5525
+ log.fail("intake", err, { phase: "bot-added-chatinfo" });
5526
+ return void 0;
5527
+ });
5528
+ const name = (info?.name ?? "").trim();
5529
+ await sendManagedCard(
5530
+ channel,
5531
+ op,
5532
+ buildJoinGroupFormCard({ chatId: evt.chatId, name }),
5533
+ void 0,
5534
+ false,
5535
+ "open_id"
5536
+ ).catch((err) => log.fail("intake", err, { phase: "bot-added-bindcard" }));
5537
+ log.info("intake", "bot-added", { chatId: evt.chatId.slice(-6), op: op.slice(-6), named: Boolean(name) });
5538
+ }).catch((err) => log.fail("intake", err, { phase: "bot-added" }));
5539
+ }
5540
+ async function onBotRemovedFromChat(chatId) {
5541
+ const project = await getProjectByChatId(chatId);
5542
+ if (!project) return;
5543
+ const removed = await removeProject(project.name);
5544
+ if (!removed) return;
5545
+ log.info("intake", "bot-removed-unbind", { name: removed.name, chatId: chatId.slice(-6) });
5546
+ if (removed.addedBy) {
5547
+ await channel.rawClient.im.v1.message.create({
5548
+ params: { receive_id_type: "open_id" },
5549
+ data: {
5550
+ receive_id: removed.addedBy,
5551
+ msg_type: "text",
5552
+ content: JSON.stringify({ text: `\u2139\uFE0F \u6211\u5DF2\u88AB\u79FB\u51FA\u7FA4\u300C${removed.name}\u300D\uFF0C\u5BF9\u5E94\u9879\u76EE\u5DF2\u81EA\u52A8\u89E3\u7ED1\u3002` })
5553
+ }
5554
+ }).catch(() => void 0);
5555
+ }
5556
+ }
5105
5557
  async function shutdown() {
5106
5558
  const live = [...sessions.values()];
5107
5559
  sessions.clear();
5108
5560
  await Promise.allSettled(live.map((t) => t.close()));
5109
5561
  log.info("bridge", "shutdown", { closed: live.length });
5110
5562
  }
5111
- return { onMessage, onComment, dispatcher, shutdown };
5563
+ return { onMessage, onComment, onBotAddedToChat, onBotRemovedFromChat, dispatcher, shutdown };
5112
5564
  }
5113
5565
  async function getThreadId(channel, messageId) {
5114
5566
  try {
@@ -5146,6 +5598,24 @@ async function startBridge(opts) {
5146
5598
  channel.on("message", orchestrator.onMessage);
5147
5599
  channel.on("cardAction", orchestrator.dispatcher.handle);
5148
5600
  channel.on("comment", orchestrator.onComment);
5601
+ channel.on("botAdded", orchestrator.onBotAddedToChat);
5602
+ try {
5603
+ const tap = channel.dispatcher;
5604
+ if (tap?.register) {
5605
+ tap.register({
5606
+ "im.chat.member.bot.deleted_v1": (raw) => {
5607
+ const ev = raw;
5608
+ const chatId = ev?.chat_id ?? ev?.event?.chat_id;
5609
+ if (chatId) void orchestrator.onBotRemovedFromChat(chatId);
5610
+ }
5611
+ });
5612
+ log.info("ws", "bot-removed-tap");
5613
+ } else {
5614
+ log.info("ws", "bot-removed-tap-unavailable");
5615
+ }
5616
+ } catch (err) {
5617
+ log.fail("ws", err, { phase: "bot-removed-tap" });
5618
+ }
5149
5619
  channel.on("reject", (evt) => log.info("intake", "reject", { reason: evt.reason, msgId: evt.messageId }));
5150
5620
  channel.on("error", (err) => log.fail("ws", err));
5151
5621
  channel.on("reconnecting", () => log.info("ws", "reconnecting"));
@@ -5164,7 +5634,7 @@ async function startBridge(opts) {
5164
5634
 
5165
5635
  // src/core/single-instance.ts
5166
5636
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "fs";
5167
- import { dirname as dirname9 } from "path";
5637
+ import { dirname as dirname10 } from "path";
5168
5638
  var BridgeAlreadyRunningError = class extends Error {
5169
5639
  constructor(pid) {
5170
5640
  super(
@@ -5193,7 +5663,7 @@ function acquireSingleInstanceLock(appId) {
5193
5663
  } catch (err) {
5194
5664
  if (err instanceof BridgeAlreadyRunningError) throw err;
5195
5665
  }
5196
- mkdirSync2(dirname9(file), { recursive: true });
5666
+ mkdirSync2(dirname10(file), { recursive: true });
5197
5667
  const record = { pid: process.pid, appId, startedAt: Date.now() };
5198
5668
  writeFileSync(file, `${JSON.stringify(record)}
5199
5669
  `, "utf8");
@@ -5262,12 +5732,14 @@ async function runStart() {
5262
5732
  return;
5263
5733
  }
5264
5734
  const status = await getServiceAdapter().install();
5265
- console.log("\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u5B89\u88C5\u5E76\u542F\u52A8\uFF08\u5F00\u673A\u81EA\u542F\u3001\u5D29\u6E83\u81EA\u52A8\u62C9\u8D77\uFF09\u3002");
5735
+ console.log(
5736
+ process.platform === "win32" ? "\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u5B89\u88C5\u5E76\u542F\u52A8\uFF08\u767B\u5F55\u81EA\u542F\uFF1B\u6CE8\u610F\uFF1AWindows \u8BA1\u5212\u4EFB\u52A1\u65E0\u5D29\u6E83\u81EA\u52A8\u62C9\u8D77\uFF09\u3002" : "\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u5B89\u88C5\u5E76\u542F\u52A8\uFF08\u5F00\u673A\u81EA\u542F\u3001\u5D29\u6E83\u81EA\u52A8\u62C9\u8D77\uFF09\u3002"
5737
+ );
5266
5738
  printStatus(status);
5267
5739
  }
5268
5740
  async function runStop() {
5269
5741
  await getServiceAdapter().uninstall();
5270
- console.log("\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u505C\u6B62\uFF0C\u5E76\u5DF2\u5173\u95ED\u5F00\u673A\u81EA\u542F\uFF08\u5DF2\u79FB\u9664 launchd plist\uFF09\u3002");
5742
+ console.log("\u2713 \u540E\u53F0\u670D\u52A1\u5DF2\u505C\u6B62\uFF0C\u5E76\u5DF2\u5173\u95ED\u81EA\u542F\uFF08\u5DF2\u79FB\u9664\u670D\u52A1\u5B9A\u4E49\uFF09\u3002");
5271
5743
  }
5272
5744
  async function runRestart() {
5273
5745
  const status = await getServiceAdapter().restart();
@@ -5281,17 +5753,18 @@ async function runLogs(follow) {
5281
5753
  await getServiceAdapter().logs(follow);
5282
5754
  }
5283
5755
  function printStatus(status) {
5284
- console.log(`plist: ${status.plistPath}`);
5756
+ console.log(`service: ${status.platformName}`);
5757
+ console.log(`path: ${status.servicePath}`);
5285
5758
  console.log(`installed: ${status.installed ? "yes" : "no"}`);
5286
- console.log(`loaded: ${status.loaded ? "yes" : "no"}`);
5759
+ console.log(`running: ${status.running ? "yes" : "no"}`);
5287
5760
  console.log(`pid: ${status.pid ?? "-"}`);
5288
5761
  console.log(`last exit: ${status.lastExit ?? "-"}`);
5289
5762
  console.log(`stdout: ${status.stdoutPath}`);
5290
5763
  console.log(`stderr: ${status.stderrPath}`);
5291
5764
  if (!status.installed) {
5292
5765
  console.log("\u63D0\u793A\uFF1A\u540E\u53F0\u670D\u52A1\u5C1A\u672A\u5B89\u88C5\uFF0C\u8FD0\u884C `feishu-codex-bridge start`\u3002");
5293
- } else if (!status.loaded) {
5294
- console.log("\u63D0\u793A\uFF1Aplist \u5DF2\u5B58\u5728\uFF0C\u4F46 launchd \u5F53\u524D\u672A\u52A0\u8F7D\uFF08\u8BD5\u8BD5 `restart`\uFF09\u3002");
5766
+ } else if (!status.running) {
5767
+ console.log("\u63D0\u793A\uFF1A\u670D\u52A1\u5DF2\u6CE8\u518C\u4F46\u5F53\u524D\u672A\u8FD0\u884C\uFF08\u8BD5\u8BD5 `restart`\uFF09\u3002");
5295
5768
  }
5296
5769
  }
5297
5770
 
@@ -5344,7 +5817,7 @@ async function runUpdate(opts = {}) {
5344
5817
  }
5345
5818
 
5346
5819
  // src/cli/commands/bot.ts
5347
- import { rm as rm4 } from "fs/promises";
5820
+ import { rm as rm5 } from "fs/promises";
5348
5821
  async function runBotInit(name) {
5349
5822
  if (!ensureCodex()) {
5350
5823
  process.exitCode = 1;
@@ -5357,6 +5830,7 @@ async function runBotInit(name) {
5357
5830
  }
5358
5831
  console.log("\n\u4E0B\u4E00\u6B65\uFF08\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u540E\u53F0\uFF0C\u9700\u624B\u52A8\u4E00\u6B21 https://open.feishu.cn/app \uFF09\uFF1A");
5359
5832
  console.log(" 1) \u4E8B\u4EF6\u4E0E\u56DE\u8C03 \u2192 \u957F\u8FDE\u63A5 \u2192 \u8BA2\u9605\uFF1Aim.message.receive_v1 / card.action.trigger / application.bot.menu_v6");
5833
+ console.log(" \uFF08\u53EF\u9009\uFF09\u300C\u52A0\u8FDB\u5DF2\u6709\u7FA4\u300D\u529F\u80FD\u518D\u8BA2\u9605\uFF1Aim.chat.member.bot.added_v1 / im.chat.member.bot.deleted_v1");
5360
5834
  console.log(" 2) \u521B\u5EFA\u5E76\u53D1\u5E03\u5E94\u7528\u7248\u672C");
5361
5835
  console.log("\n`bot list` \u67E5\u770B\u5168\u90E8\uFF1B`bot use <\u540D>` \u5207\u6362\u5F53\u524D\uFF1B`run` \u524D\u53F0\u8DD1 / `start` \u540E\u53F0\u5E38\u9A7B\u3002\n");
5362
5836
  }
@@ -5398,7 +5872,7 @@ async function runBotRm(name) {
5398
5872
  }
5399
5873
  const after = await removeBot(bot2.appId);
5400
5874
  await removeSecret(secretKeyForApp(bot2.appId));
5401
- await rm4(botDir(bot2.appId), { recursive: true, force: true });
5875
+ await rm5(botDir(bot2.appId), { recursive: true, force: true });
5402
5876
  console.log(`\u2713 \u5DF2\u79FB\u9664\u673A\u5668\u4EBA\u300C${bot2.name}\u300D(${bot2.appId})\uFF1A\u6CE8\u518C\u8868 + \u5BC6\u94A5 + \u72B6\u6001\u76EE\u5F55(projects/sessions)\u3002`);
5403
5877
  if (after.bots.length === 0) {
5404
5878
  console.log(" \u5DF2\u65E0\u4EFB\u4F55\u673A\u5668\u4EBA\uFF0C`bot init` \u91CD\u65B0\u521B\u5EFA\u3002");
@@ -5456,15 +5930,15 @@ async function secretsRemove(id) {
5456
5930
  console.log(ok ? `\u2713 \u5DF2\u5220\u9664: ${id}` : `\u672A\u627E\u5230: ${id}`);
5457
5931
  }
5458
5932
  function readStdin() {
5459
- return new Promise((resolve6) => {
5933
+ return new Promise((resolve7) => {
5460
5934
  let data = "";
5461
5935
  if (process.stdin.isTTY) {
5462
- resolve6("");
5936
+ resolve7("");
5463
5937
  return;
5464
5938
  }
5465
5939
  process.stdin.setEncoding("utf8");
5466
5940
  process.stdin.on("data", (c) => data += c);
5467
- process.stdin.on("end", () => resolve6(data));
5941
+ process.stdin.on("end", () => resolve7(data));
5468
5942
  });
5469
5943
  }
5470
5944