codex-to-im 1.0.42 → 1.0.44

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.
@@ -4593,6 +4593,9 @@ function normalizeChannelId(value) {
4593
4593
  }
4594
4594
 
4595
4595
  // src/config.ts
4596
+ function isSupportedChannelProvider(value) {
4597
+ return value === "feishu" || value === "weixin";
4598
+ }
4596
4599
  function toFeishuConfig(channel) {
4597
4600
  return channel?.provider === "feishu" ? channel.config : void 0;
4598
4601
  }
@@ -4668,6 +4671,8 @@ function readConfigV2File() {
4668
4671
  try {
4669
4672
  const parsed = JSON.parse(fs.readFileSync(CONFIG_V2_PATH, "utf-8"));
4670
4673
  if (parsed && parsed.schemaVersion === 2 && parsed.runtime && Array.isArray(parsed.channels)) {
4674
+ parsed.runtime.provider = normalizeRuntimeProvider(parsed.runtime.provider);
4675
+ parsed.channels = normalizeChannelInstances(parsed.channels);
4671
4676
  return parsed;
4672
4677
  }
4673
4678
  return null;
@@ -4687,9 +4692,32 @@ function defaultAliasForProvider(provider) {
4687
4692
  function buildDefaultChannelId(provider) {
4688
4693
  return `${provider}-default`;
4689
4694
  }
4695
+ function normalizeRuntimeProvider(_value) {
4696
+ return "codex";
4697
+ }
4698
+ function normalizeChannelInstances(value) {
4699
+ if (!Array.isArray(value)) return [];
4700
+ return value.flatMap((entry) => {
4701
+ if (!entry || typeof entry !== "object") return [];
4702
+ const record = entry;
4703
+ if (!isSupportedChannelProvider(record.provider)) return [];
4704
+ const provider = record.provider;
4705
+ const config = record.config && typeof record.config === "object" ? record.config : {};
4706
+ const timestamp = nowIso();
4707
+ return [{
4708
+ id: normalizeChannelId(
4709
+ typeof record.id === "string" && record.id.trim() ? record.id : buildDefaultChannelId(provider)
4710
+ ),
4711
+ alias: typeof record.alias === "string" && record.alias.trim() ? record.alias.trim() : defaultAliasForProvider(provider),
4712
+ provider,
4713
+ enabled: record.enabled === true,
4714
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : timestamp,
4715
+ updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : timestamp,
4716
+ config
4717
+ }];
4718
+ });
4719
+ }
4690
4720
  function migrateLegacyEnvToV2(env) {
4691
- const rawRuntime = env.get("CTI_RUNTIME") || "codex";
4692
- const runtime = ["claude", "codex", "auto"].includes(rawRuntime) ? rawRuntime : "codex";
4693
4721
  const enabledChannels = splitCsv(env.get("CTI_ENABLED_CHANNELS")) ?? ["feishu"];
4694
4722
  const timestamp = nowIso();
4695
4723
  const channels = [];
@@ -4736,7 +4764,7 @@ function migrateLegacyEnvToV2(env) {
4736
4764
  return {
4737
4765
  schemaVersion: 2,
4738
4766
  runtime: {
4739
- provider: runtime,
4767
+ provider: "codex",
4740
4768
  defaultWorkspaceRoot: expandHomePath(env.get("CTI_DEFAULT_WORKSPACE_ROOT")) || void 0,
4741
4769
  defaultModel: env.get("CTI_DEFAULT_MODEL") || void 0,
4742
4770
  defaultMode: env.get("CTI_DEFAULT_MODE") || "code",
@@ -4747,8 +4775,7 @@ function migrateLegacyEnvToV2(env) {
4747
4775
  codexSandboxMode: parseSandboxMode(env.get("CTI_CODEX_SANDBOX_MODE")) ?? "workspace-write",
4748
4776
  codexReasoningEffort: parseReasoningEffort(env.get("CTI_CODEX_REASONING_EFFORT")) ?? "medium",
4749
4777
  uiAllowLan: env.get("CTI_UI_ALLOW_LAN") === "true",
4750
- uiAccessToken: env.get("CTI_UI_ACCESS_TOKEN") || void 0,
4751
- autoApprove: env.get("CTI_AUTO_APPROVE") === "true"
4778
+ uiAccessToken: env.get("CTI_UI_ACCESS_TOKEN") || void 0
4752
4779
  },
4753
4780
  channels
4754
4781
  };
@@ -4775,13 +4802,13 @@ function expandConfig(v2) {
4775
4802
  codexSandboxMode: v2.runtime.codexSandboxMode ?? "workspace-write",
4776
4803
  codexReasoningEffort: v2.runtime.codexReasoningEffort ?? "medium",
4777
4804
  uiAllowLan: v2.runtime.uiAllowLan === true,
4778
- uiAccessToken: v2.runtime.uiAccessToken || void 0,
4779
- autoApprove: v2.runtime.autoApprove === true
4805
+ uiAccessToken: v2.runtime.uiAccessToken || void 0
4780
4806
  };
4781
4807
  }
4782
4808
  function buildV2FileFromExpandedConfig(config, current) {
4783
4809
  const hasExplicitChannels = Array.isArray(config.channels);
4784
4810
  let channels = hasExplicitChannels ? [...config.channels || []] : [...current?.channels || []];
4811
+ channels = normalizeChannelInstances(channels);
4785
4812
  return {
4786
4813
  schemaVersion: 2,
4787
4814
  runtime: {
@@ -4796,8 +4823,7 @@ function buildV2FileFromExpandedConfig(config, current) {
4796
4823
  codexSandboxMode: config.codexSandboxMode,
4797
4824
  codexReasoningEffort: config.codexReasoningEffort,
4798
4825
  uiAllowLan: config.uiAllowLan,
4799
- uiAccessToken: config.uiAccessToken,
4800
- autoApprove: config.autoApprove
4826
+ uiAccessToken: config.uiAccessToken
4801
4827
  },
4802
4828
  channels: channels.map((channel) => ({
4803
4829
  ...channel,
@@ -4827,8 +4853,7 @@ function loadConfig() {
4827
4853
  codexSkipGitRepoCheck: true,
4828
4854
  codexSandboxMode: "workspace-write",
4829
4855
  codexReasoningEffort: "medium",
4830
- uiAllowLan: false,
4831
- autoApprove: false
4856
+ uiAllowLan: false
4832
4857
  },
4833
4858
  channels: []
4834
4859
  };
@@ -4894,7 +4919,6 @@ function saveConfig(config) {
4894
4919
  out += formatEnvLine("CTI_WEIXIN_COMMAND_MARKDOWN_ENABLED", String(weixinConfig.feedbackMarkdownEnabled));
4895
4920
  }
4896
4921
  }
4897
- out += formatEnvLine("CTI_AUTO_APPROVE", String(next.runtime.autoApprove === true));
4898
4922
  ensureConfigDir();
4899
4923
  const tmpPath = CONFIG_PATH + ".tmp";
4900
4924
  fs.writeFileSync(tmpPath, out, { mode: 384 });
@@ -4908,13 +4932,14 @@ function findChannelInstance(channelId, config) {
4908
4932
  }
4909
4933
  function configToSettings(config) {
4910
4934
  const m = /* @__PURE__ */ new Map();
4935
+ const channels = normalizeChannelInstances(config.channels || []);
4911
4936
  const current = {
4912
4937
  schemaVersion: 2,
4913
4938
  runtime: {
4914
4939
  provider: config.runtime,
4915
4940
  defaultMode: config.defaultMode
4916
4941
  },
4917
- channels: config.channels || []
4942
+ channels
4918
4943
  };
4919
4944
  const feishu = getChannelByProvider(current, "feishu");
4920
4945
  const weixin = getChannelByProvider(current, "weixin");
@@ -4959,23 +4984,8 @@ function configToSettings(config) {
4959
4984
  );
4960
4985
  m.set(
4961
4986
  "bridge_channel_instances_json",
4962
- JSON.stringify(config.channels || [])
4963
- );
4964
- m.set(
4965
- "bridge_telegram_enabled",
4966
- config.enabledChannels.includes("telegram") ? "true" : "false"
4967
- );
4968
- if (config.tgBotToken) m.set("telegram_bot_token", config.tgBotToken);
4969
- if (config.tgAllowedUsers) m.set("telegram_bridge_allowed_users", config.tgAllowedUsers.join(","));
4970
- if (config.tgChatId) m.set("telegram_chat_id", config.tgChatId);
4971
- m.set(
4972
- "bridge_discord_enabled",
4973
- config.enabledChannels.includes("discord") ? "true" : "false"
4987
+ JSON.stringify(channels)
4974
4988
  );
4975
- if (config.discordBotToken) m.set("bridge_discord_bot_token", config.discordBotToken);
4976
- if (config.discordAllowedUsers) m.set("bridge_discord_allowed_users", config.discordAllowedUsers.join(","));
4977
- if (config.discordAllowedChannels) m.set("bridge_discord_allowed_channels", config.discordAllowedChannels.join(","));
4978
- if (config.discordAllowedGuilds) m.set("bridge_discord_allowed_guilds", config.discordAllowedGuilds.join(","));
4979
4989
  m.set(
4980
4990
  "bridge_feishu_enabled",
4981
4991
  feishu?.enabled === true ? "true" : "false"
@@ -4992,19 +5002,6 @@ function configToSettings(config) {
4992
5002
  "bridge_feishu_command_markdown_enabled",
4993
5003
  feishuConfig?.feedbackMarkdownEnabled === false ? "false" : "true"
4994
5004
  );
4995
- m.set(
4996
- "bridge_qq_enabled",
4997
- config.enabledChannels.includes("qq") ? "true" : "false"
4998
- );
4999
- if (config.qqAppId) m.set("bridge_qq_app_id", config.qqAppId);
5000
- if (config.qqAppSecret) m.set("bridge_qq_app_secret", config.qqAppSecret);
5001
- if (config.qqAllowedUsers) m.set("bridge_qq_allowed_users", config.qqAllowedUsers.join(","));
5002
- if (config.qqImageEnabled !== void 0) {
5003
- m.set("bridge_qq_image_enabled", String(config.qqImageEnabled));
5004
- }
5005
- if (config.qqMaxImageSize !== void 0) {
5006
- m.set("bridge_qq_max_image_size", String(config.qqMaxImageSize));
5007
- }
5008
5005
  m.set(
5009
5006
  "bridge_weixin_enabled",
5010
5007
  weixin?.enabled === true ? "true" : "false"
@@ -5414,6 +5411,42 @@ function isArchivedDesktopThread(threadId) {
5414
5411
 
5415
5412
  // src/session-bindings.ts
5416
5413
  import path3 from "node:path";
5414
+
5415
+ // src/lib/bridge/binding-audit.ts
5416
+ function describeBinding(binding) {
5417
+ if (!binding) return "none";
5418
+ const parts = [
5419
+ `session=${binding.codepilotSessionId}`,
5420
+ `sdk=${binding.sdkSessionId || "-"}`,
5421
+ `mode=${binding.mode}`
5422
+ ];
5423
+ if (binding.workingDirectory) {
5424
+ parts.push(`cwd=${binding.workingDirectory}`);
5425
+ }
5426
+ return parts.join(", ");
5427
+ }
5428
+ function recordBindingChange(store, input) {
5429
+ const from = describeBinding(input.fromBinding);
5430
+ const to = describeBinding(input.toBinding);
5431
+ const details = [
5432
+ `action=${input.action}`,
5433
+ `from=[${from}]`,
5434
+ `to=[${to}]`
5435
+ ];
5436
+ if (input.source) details.push(`source=${input.source}`);
5437
+ if (input.reason) details.push(`reason=${input.reason}`);
5438
+ store.insertAuditLog({
5439
+ channelType: input.address.channelType,
5440
+ channelProvider: input.address.channelProvider || input.toBinding?.channelProvider || input.fromBinding?.channelProvider,
5441
+ channelAlias: input.address.channelAlias || input.toBinding?.channelAlias || input.fromBinding?.channelAlias,
5442
+ chatId: input.address.chatId,
5443
+ direction: "inbound",
5444
+ messageId: input.messageId || `binding-change:${Date.now()}`,
5445
+ summary: `Binding change: ${details.join("; ")}`
5446
+ });
5447
+ }
5448
+
5449
+ // src/session-bindings.ts
5417
5450
  function asChannelProvider(value) {
5418
5451
  return value === "feishu" || value === "weixin" ? value : void 0;
5419
5452
  }
@@ -5591,6 +5624,7 @@ function updateBindingTarget(store, bindingId, targetKey) {
5591
5624
  if (!binding) {
5592
5625
  throw new Error("Binding not found.");
5593
5626
  }
5627
+ const fromBinding = { ...binding };
5594
5628
  if (targetKey.startsWith("desktop:")) {
5595
5629
  const threadId = targetKey.slice("desktop:".length);
5596
5630
  const desktop = getDesktopSessionByThreadId(threadId);
@@ -5616,6 +5650,20 @@ function updateBindingTarget(store, bindingId, targetKey) {
5616
5650
  } else {
5617
5651
  throw new Error("Unsupported target.");
5618
5652
  }
5653
+ const toBinding = store.getChannelBinding(binding.channelType, binding.chatId);
5654
+ recordBindingChange(store, {
5655
+ action: "web_switch",
5656
+ address: {
5657
+ channelType: binding.channelType,
5658
+ channelProvider: binding.channelProvider,
5659
+ channelAlias: binding.channelAlias,
5660
+ chatId: binding.chatId
5661
+ },
5662
+ fromBinding,
5663
+ toBinding,
5664
+ source: "web_ui",
5665
+ reason: `target=${targetKey}`
5666
+ });
5619
5667
  const updated = listBindingSummaries(store).find((item) => item.id === bindingId);
5620
5668
  if (!updated) {
5621
5669
  throw new Error("Updated binding not found.");
@@ -5627,7 +5675,20 @@ function removeBinding(store, bindingId) {
5627
5675
  if (!binding) {
5628
5676
  throw new Error("Binding not found.");
5629
5677
  }
5678
+ const fromBinding = { ...binding };
5630
5679
  store.deleteChannelBinding(bindingId);
5680
+ recordBindingChange(store, {
5681
+ action: "web_unbind",
5682
+ address: {
5683
+ channelType: binding.channelType,
5684
+ channelProvider: binding.channelProvider,
5685
+ channelAlias: binding.channelAlias,
5686
+ chatId: binding.chatId
5687
+ },
5688
+ fromBinding,
5689
+ toBinding: null,
5690
+ source: "web_ui"
5691
+ });
5631
5692
  }
5632
5693
 
5633
5694
  // src/service-manager.ts
@@ -6471,7 +6532,7 @@ var JsonFileStore = class {
6471
6532
  this.persistSessions();
6472
6533
  }
6473
6534
  }
6474
- updateSession(sessionId, updates) {
6535
+ updateSession(sessionId, updates, options) {
6475
6536
  this.reloadSessions();
6476
6537
  const session = this.sessions.get(sessionId);
6477
6538
  if (!session) return;
@@ -6479,7 +6540,7 @@ var JsonFileStore = class {
6479
6540
  ...session,
6480
6541
  ...updates,
6481
6542
  id: session.id,
6482
- updated_at: now()
6543
+ updated_at: options?.touch === false ? session.updated_at : now()
6483
6544
  };
6484
6545
  this.sessions.set(sessionId, next);
6485
6546
  this.persistSessions();
@@ -7712,8 +7773,7 @@ function configToPayload(config) {
7712
7773
  codexReasoningEffort: config.codexReasoningEffort || "medium",
7713
7774
  uiAllowLan: config.uiAllowLan === true,
7714
7775
  uiAccessToken: config.uiAccessToken || "",
7715
- autoApprove: config.autoApprove === true,
7716
- channels: (config.channels || []).map(channelToPayload)
7776
+ channels: (config.channels || []).filter((channel) => isSupportedChannelProvider(channel.provider)).map(channelToPayload)
7717
7777
  };
7718
7778
  }
7719
7779
  function mergeConfig(payload) {
@@ -7724,7 +7784,7 @@ function mergeConfig(payload) {
7724
7784
  const uiAccessToken = requestedUiAccessToken || current.uiAccessToken || (uiAllowLan ? generateAccessToken() : void 0);
7725
7785
  return {
7726
7786
  ...current,
7727
- runtime: payload.runtime === "claude" || payload.runtime === "auto" ? payload.runtime : "codex",
7787
+ runtime: "codex",
7728
7788
  enabledChannels: current.enabledChannels,
7729
7789
  defaultWorkspaceRoot: asString(payload.defaultWorkspaceRoot),
7730
7790
  defaultModel: rawDefaultModel === void 0 ? current.defaultModel : rawDefaultModel === "" ? void 0 : availableCodexModelSlugs.has(rawDefaultModel) ? rawDefaultModel : current.defaultModel,
@@ -7737,7 +7797,6 @@ function mergeConfig(payload) {
7737
7797
  codexReasoningEffort: payload.codexReasoningEffort === "minimal" || payload.codexReasoningEffort === "low" || payload.codexReasoningEffort === "high" || payload.codexReasoningEffort === "xhigh" ? payload.codexReasoningEffort : "medium",
7738
7798
  uiAllowLan,
7739
7799
  uiAccessToken,
7740
- autoApprove: payload.autoApprove === true,
7741
7800
  channels: current.channels
7742
7801
  };
7743
7802
  }
@@ -9553,8 +9612,6 @@ function renderHtml() {
9553
9612
  Runtime
9554
9613
  <select id="runtime">
9555
9614
  <option value="codex" selected>codex</option>
9556
- <option value="auto">auto</option>
9557
- <option value="claude">claude</option>
9558
9615
  </select>
9559
9616
  </label>
9560
9617
  <label>
@@ -9570,11 +9627,11 @@ function renderHtml() {
9570
9627
  <input id="historyMessageLimit" type="number" min="1" max="20" value="8" />
9571
9628
  </label>
9572
9629
  <label>
9573
- \u9759\u9ED8\u68C0\u6D4B\u542F\u52A8\u65F6\u957F\uFF08\u79D2\uFF09
9630
+ \u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA\u663E\u793A\u542F\u52A8\u65F6\u957F\uFF08\u79D2\uFF09
9574
9631
  <input id="streamStatusIdleStartSeconds" type="number" min="1" value="180" />
9575
9632
  </label>
9576
9633
  <label>
9577
- \u9759\u9ED8\u68C0\u6D4B\u95F4\u9694\uFF08\u79D2\uFF09
9634
+ \u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA\u68C0\u67E5\u95F4\u9694\uFF08\u79D2\uFF09
9578
9635
  <input id="streamStatusCheckIntervalSeconds" type="number" min="1" value="10" />
9579
9636
  </label>
9580
9637
  </div>
@@ -9606,11 +9663,8 @@ function renderHtml() {
9606
9663
  </select>
9607
9664
  </label>
9608
9665
  </div>
9609
- <div class="small">\u672A\u7ED1\u5B9A\u7684 IM \u804A\u5929\u4F1A\u5148\u8FDB\u5165\u4E34\u65F6\u8349\u7A3F\u7EBF\u7A0B\uFF08\u7B49\u540C <code>/t 0</code>\uFF09\uFF1B\u201C\u9ED8\u8BA4\u5DE5\u4F5C\u7A7A\u95F4\u201D\u53EA\u7528\u4E8E <code>/new proj1</code> \u8FD9\u7C7B\u76F8\u5BF9\u9879\u76EE\u540D\u3002\u7559\u7A7A\u65F6\u4F1A\u6309\u5F53\u524D\u7CFB\u7EDF\u81EA\u52A8\u56DE\u9000\u5230 <code>~/cx2im</code>\u3002\u9ED8\u8BA4\u6A21\u578B\u5019\u9009\u9879\u6765\u81EA\u542F\u52A8\u65F6\u8BFB\u53D6\u7684 Codex \u6A21\u578B\u7F13\u5B58\uFF1A\u9690\u85CF\u6A21\u578B\u4E0D\u4F1A\u5C55\u793A\uFF0CCLI only \u6A21\u578B\u4F1A\u6807\u6210\u201C\u4EC5 IM / CLI\u201D\u3002\u7559\u7A7A\u5219\u7EE7\u7EED\u8DDF\u968F Codex \u5F53\u524D\u9ED8\u8BA4\u6A21\u578B\u3002\u6587\u4EF6\u7CFB\u7EDF\u6743\u9650\u662F\u5168\u5C40\u9ED8\u8BA4\u503C\uFF0C\u601D\u8003\u7EA7\u522B\u53EF\u5728 IM \u4F1A\u8BDD\u91CC\u518D\u5355\u72EC\u8986\u76D6\u3002\u9759\u9ED8\u68C0\u6D4B\u914D\u7F6E\u53EA\u5F71\u54CD\u98DE\u4E66\u957F\u4EFB\u52A1\u5E95\u90E8\u201C\u6700\u8FD1 X \u65E0\u65B0\u8F93\u51FA\u201D\u7684\u51FA\u73B0\u65F6\u673A\u3002</div>
9610
- <div class="small">\u5F53\u524D\u9700\u8981\u91CD\u542F Bridge \u7684\u914D\u7F6E\uFF1A<code>Runtime</code>\u3001<code>\u81EA\u52A8\u6279\u51C6\u5DE5\u5177\u6743\u9650</code>\u3001<code>\u5141\u8BB8\u5728\u672A\u4FE1\u4EFB Git \u76EE\u5F55\u8FD0\u884C Codex</code>\u3002\u901A\u9053\u5B9E\u4F8B\u7684\u63A5\u5165\u914D\u7F6E\u8BF7\u5728\u201C\u901A\u9053\u201D\u9875\u7EF4\u62A4\u3002</div>
9611
- <div class="checkbox-row">
9612
- <label class="checkbox"><input id="autoApprove" type="checkbox" /> \u81EA\u52A8\u6279\u51C6\u5DE5\u5177\u6743\u9650</label>
9613
- </div>
9666
+ <div class="small">\u672A\u7ED1\u5B9A\u7684 IM \u804A\u5929\u4F1A\u5148\u8FDB\u5165\u4E34\u65F6\u8349\u7A3F\u7EBF\u7A0B\uFF08\u7B49\u540C <code>/t 0</code>\uFF09\uFF1B\u201C\u9ED8\u8BA4\u5DE5\u4F5C\u7A7A\u95F4\u201D\u53EA\u7528\u4E8E <code>/new proj1</code> \u8FD9\u7C7B\u76F8\u5BF9\u9879\u76EE\u540D\u3002\u7559\u7A7A\u65F6\u4F1A\u6309\u5F53\u524D\u7CFB\u7EDF\u81EA\u52A8\u56DE\u9000\u5230 <code>~/cx2im</code>\u3002\u9ED8\u8BA4\u6A21\u578B\u5019\u9009\u9879\u6765\u81EA\u542F\u52A8\u65F6\u8BFB\u53D6\u7684 Codex \u6A21\u578B\u7F13\u5B58\uFF1A\u9690\u85CF\u6A21\u578B\u4E0D\u4F1A\u5C55\u793A\uFF0CCLI only \u6A21\u578B\u4F1A\u6807\u6210\u201C\u4EC5 IM / CLI\u201D\u3002\u7559\u7A7A\u5219\u7EE7\u7EED\u8DDF\u968F Codex \u5F53\u524D\u9ED8\u8BA4\u6A21\u578B\u3002\u6587\u4EF6\u7CFB\u7EDF\u6743\u9650\u662F\u5168\u5C40\u9ED8\u8BA4\u503C\uFF0C\u601D\u8003\u7EA7\u522B\u53EF\u5728 IM \u4F1A\u8BDD\u91CC\u518D\u5355\u72EC\u8986\u76D6\u3002\u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA\u914D\u7F6E\u53EA\u5F71\u54CD\u98DE\u4E66\u957F\u4EFB\u52A1\u5E95\u90E8\u201C\u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA X\u201D\u7684\u51FA\u73B0\u65F6\u673A\u3002</div>
9667
+ <div class="small">\u5F53\u524D\u9700\u8981\u91CD\u542F Bridge \u7684\u914D\u7F6E\uFF1A<code>Runtime</code>\u3001<code>\u5141\u8BB8\u5728\u672A\u4FE1\u4EFB Git \u76EE\u5F55\u8FD0\u884C Codex</code>\u3002\u901A\u9053\u5B9E\u4F8B\u7684\u63A5\u5165\u914D\u7F6E\u8BF7\u5728\u201C\u901A\u9053\u201D\u9875\u7EF4\u62A4\u3002</div>
9614
9668
  <div class="checkbox-row">
9615
9669
  <label class="checkbox"><input id="codexSkipGitRepoCheck" type="checkbox" checked /> \u5141\u8BB8\u5728\u672A\u4FE1\u4EFB Git \u76EE\u5F55\u8FD0\u884C Codex</label>
9616
9670
  </div>
@@ -9935,7 +9989,6 @@ function renderHtml() {
9935
9989
  codexReasoningEffort: document.getElementById('codexReasoningEffort').value,
9936
9990
  uiAllowLan: document.getElementById('uiAllowLan').checked,
9937
9991
  uiAccessToken: document.getElementById('uiAccessToken').value,
9938
- autoApprove: document.getElementById('autoApprove').checked,
9939
9992
  };
9940
9993
  }
9941
9994
 
@@ -10105,20 +10158,18 @@ function renderHtml() {
10105
10158
  defaultModel: '\u9ED8\u8BA4\u6A21\u578B',
10106
10159
  defaultMode: '\u9ED8\u8BA4\u6A21\u5F0F',
10107
10160
  historyMessageLimit: '/history \u8FD4\u56DE\u6761\u6570',
10108
- streamStatusIdleStartSeconds: '\u9759\u9ED8\u68C0\u6D4B\u542F\u52A8\u65F6\u957F',
10109
- streamStatusCheckIntervalSeconds: '\u9759\u9ED8\u68C0\u6D4B\u95F4\u9694',
10161
+ streamStatusIdleStartSeconds: '\u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA\u663E\u793A\u542F\u52A8\u65F6\u957F',
10162
+ streamStatusCheckIntervalSeconds: '\u4E0A\u6B21\u54CD\u5E94\u8DDD\u4ECA\u68C0\u67E5\u95F4\u9694',
10110
10163
  codexSkipGitRepoCheck: '\u5141\u8BB8\u5728\u672A\u4FE1\u4EFB Git \u76EE\u5F55\u8FD0\u884C Codex',
10111
10164
  codexSandboxMode: 'Codex \u6587\u4EF6\u7CFB\u7EDF\u6743\u9650',
10112
10165
  codexReasoningEffort: 'Codex \u601D\u8003\u7EA7\u522B',
10113
10166
  uiAllowLan: '\u5141\u8BB8\u5C40\u57DF\u7F51\u8BBF\u95EE Web \u63A7\u5236\u53F0',
10114
10167
  uiAccessToken: '\u5C40\u57DF\u7F51\u8BBF\u95EE token',
10115
- autoApprove: '\u81EA\u52A8\u6279\u51C6\u5DE5\u5177\u6743\u9650',
10116
10168
  };
10117
10169
 
10118
10170
  const BRIDGE_RESTART_FIELDS = new Set([
10119
10171
  'runtime',
10120
10172
  'codexSkipGitRepoCheck',
10121
- 'autoApprove',
10122
10173
  ]);
10123
10174
 
10124
10175
  const AUTO_SYNC_FIELDS = new Set([]);
@@ -10710,7 +10761,6 @@ function renderHtml() {
10710
10761
  document.getElementById('codexReasoningEffort').value = config.codexReasoningEffort || 'medium';
10711
10762
  document.getElementById('uiAllowLan').checked = config.uiAllowLan === true;
10712
10763
  document.getElementById('uiAccessToken').value = config.uiAccessToken || '';
10713
- document.getElementById('autoApprove').checked = config.autoApprove === true;
10714
10764
  renderUiAccess();
10715
10765
  ensureActiveChannelId();
10716
10766
  renderChannelsWorkspace();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-to-im",
3
- "version": "1.0.42",
3
+ "version": "1.0.44",
4
4
  "description": "Installable Codex-to-IM bridge with local setup UI and background service",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/zhangle1987/codex-to-im#readme",
@@ -40,10 +40,8 @@
40
40
  "prepublishOnly": "npm run typecheck && npm run build"
41
41
  },
42
42
  "dependencies": {
43
- "@anthropic-ai/claude-agent-sdk": "^0.2.62",
44
43
  "@larksuiteoapi/node-sdk": "^1.59.0",
45
- "@openai/codex-sdk": "^0.122.0",
46
- "discord.js": "^14.25.1",
44
+ "@openai/codex-sdk": "^0.124.0",
47
45
  "markdown-it": "^14.1.1",
48
46
  "qrcode": "^1.5.4",
49
47
  "ws": "^8.18.0"
@@ -4,97 +4,6 @@ Detailed step-by-step guides for each IM platform. Referenced by the `setup` and
4
4
 
5
5
  ---
6
6
 
7
- ## Telegram
8
-
9
- ### Bot Token
10
-
11
- **How to get a Telegram Bot Token:**
12
- 1. Open Telegram and search for `@BotFather`
13
- 2. Send `/newbot` to create a new bot
14
- 3. Follow the prompts: choose a display name and a username (must end in `bot`)
15
- 4. BotFather will reply with a token like `7823456789:AAF-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
16
- 5. Copy the full token and paste it here
17
-
18
- **Recommended bot settings** (send these commands to @BotFather):
19
- - `/setprivacy` → choose your bot → `Disable` (so the bot can read group messages, only needed for group use)
20
- - `/setcommands` → set commands like `new - Start new session`, `mode - Switch mode`
21
-
22
- Token format: `数字:字母数字字符串` (e.g. `7823456789:AAF-xxx...xxx`)
23
-
24
- ### Chat ID
25
-
26
- **How to get your Telegram Chat ID:**
27
- 1. Start a chat with your bot (search for the bot's username and click **Start**)
28
- 2. Send any message to the bot (e.g. "hello")
29
- 3. Open this URL in your browser (replace `YOUR_BOT_TOKEN` with your actual bot token):
30
- `https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates`
31
- 4. In the JSON response, find `"chat":{"id":123456789,...}` — that number is your Chat ID
32
- 5. For group chats, the Chat ID is a negative number (e.g. `-1001234567890`)
33
-
34
- **Why this matters:** The bot uses Chat ID for authorization. If neither Chat ID nor Allowed User IDs are configured, the bot will reject all incoming messages.
35
-
36
- ### Allowed User IDs (optional)
37
-
38
- **How to find your Telegram User ID:**
39
- 1. Search for `@userinfobot` on Telegram and start a chat
40
- 2. It will reply with your User ID (a number like `123456789`)
41
- 3. Alternatively, forward a message from yourself to `@userinfobot`
42
-
43
- Enter comma-separated IDs to restrict access (recommended for security).
44
- Leave empty to allow anyone who can message the bot.
45
-
46
- ---
47
-
48
- ## Discord
49
-
50
- ### Bot Token
51
-
52
- **How to create a Discord Bot and get the token:**
53
- 1. Go to https://discord.com/developers/applications
54
- 2. Click **"New Application"** → give it a name → click **"Create"**
55
- 3. Go to the **"Bot"** tab on the left sidebar
56
- 4. Click **"Reset Token"** → copy the token (you can only see it once!)
57
-
58
- **Required bot settings (on the Bot tab):**
59
- - Under **Privileged Gateway Intents**, enable:
60
- - ✅ **Message Content Intent** (required to read message text)
61
-
62
- **Invite the bot to your server:**
63
- 1. Go to the **"OAuth2"** tab → **"URL Generator"**
64
- 2. Under **Scopes**, check: `bot`
65
- 3. Under **Bot Permissions**, check: `Send Messages`, `Read Message History`, `View Channels`
66
- 4. Copy the generated URL at the bottom and open it in your browser
67
- 5. Select the server and click **"Authorize"**
68
-
69
- Token format: a long base64-like string (e.g. `MTIzNDU2Nzg5.Gxxxxx.xxxxxxxxxxxxxxxxxxxxxxxx`)
70
-
71
- ### Allowed User IDs
72
-
73
- **How to find Discord User IDs:**
74
- 1. In Discord, go to Settings → Advanced → enable **Developer Mode**
75
- 2. Right-click on any user → **"Copy User ID"**
76
-
77
- Enter comma-separated IDs.
78
-
79
- **Why this matters:** The bot uses a default-deny policy. If neither Allowed User IDs nor Allowed Channel IDs are configured, the bot will silently reject all incoming messages. You must set at least one.
80
-
81
- ### Allowed Channel IDs (optional)
82
-
83
- **How to find Discord Channel IDs:**
84
- 1. With Developer Mode enabled, right-click on any channel → **"Copy Channel ID"**
85
-
86
- Enter comma-separated IDs to restrict the bot to specific channels.
87
- Leave empty to allow all channels the bot can see.
88
-
89
- ### Allowed Guild (Server) IDs (optional)
90
-
91
- **How to find Discord Server IDs:**
92
- 1. With Developer Mode enabled, right-click on the server icon → **"Copy Server ID"**
93
-
94
- Enter comma-separated IDs. Leave empty to allow all servers the bot is in.
95
-
96
- ---
97
-
98
7
  ## Feishu / Lark
99
8
 
100
9
  ### App ID and App Secret
@@ -138,11 +47,11 @@ Enter comma-separated IDs. Leave empty to allow all servers the bot is in.
138
47
  }
139
48
  ```
140
49
 
141
- 4. Click **"Save"** to apply all permissions
142
-
143
- If the batch import UI is not available, add each scope manually via the search box.
144
-
145
- > **Important:** If `cardkit:card:write` is missing, enabling Feishu streaming in the local workbench will not work. The bridge will log Feishu error `99991672` and fall back to a normal final-result message.
50
+ 4. Click **"Save"** to apply all permissions
51
+
52
+ If the batch import UI is not available, add each scope manually via the search box.
53
+
54
+ > **Important:** If `cardkit:card:write` is missing, enabling Feishu streaming in the local workbench will not work. The bridge will log Feishu error `99991672` and fall back to a normal final-result message.
146
55
 
147
56
  **Step B — Enable the bot**
148
57
 
@@ -161,9 +70,9 @@ If the batch import UI is not available, add each scope manually via the search
161
70
 
162
71
  > The bridge service must be running before configuring events. Feishu validates the WebSocket connection when saving event subscription — if the bridge is not running, you'll get "未检测到应用连接信息" (connection not detected) error.
163
72
 
164
- **Step D — Start the bridge service**
165
-
166
- Start the bridge from the local `codex-to-im` workbench. This establishes the WebSocket long connection that Feishu needs to detect.
73
+ **Step D — Start the bridge service**
74
+
75
+ Start the bridge from the local `codex-to-im` workbench. This establishes the WebSocket long connection that Feishu needs to detect.
167
76
 
168
77
  **Step E — Configure Events & Callbacks (long connection)**
169
78
 
@@ -192,21 +101,21 @@ If you already have a Feishu app configured, you need to:
192
101
  - `im:message:update` — Real-time card content updates
193
102
  - `im:message.reactions:read`, `im:message.reactions:write_only` — Typing indicator
194
103
  2. **Publish a new version** — Permission changes only take effect after a new version is approved
195
- 3. **Start (or restart) the bridge** — Start it from the local `codex-to-im` workbench so the WebSocket connection is active
104
+ 3. **Start (or restart) the bridge** — Start it from the local `codex-to-im` workbench so the WebSocket connection is active
196
105
  4. **Add callback**: Go to Events & Callbacks, add `card.action.trigger` callback (card interaction for permission buttons). This step requires the bridge to be running — Feishu validates the WebSocket connection when saving.
197
106
  5. **Publish again** — The new callback requires another version publish + admin approval
198
- 6. **Restart the bridge** — Stop and start it again from the local `codex-to-im` workbench to pick up the new capabilities
199
-
200
- ### Current Feishu streaming behavior with Codex runtime
201
-
202
- Even after the Feishu permissions are correct, the current `codex` runtime does **not** guarantee token-by-token text streaming into the Feishu card.
203
-
204
- As of **2026-03-24**, the `Codex CLI / SDK` event stream typically emits assistant text when the `agent_message` item completes, rather than as token deltas. In practice, Feishu streaming cards are best understood as:
205
-
206
- - early `Thinking` / tool progress updates
207
- - final response text written into the card at completion
208
-
209
- So if you see the final answer appear all at once after the card was created successfully, that is currently expected behavior with `codex`.
107
+ 6. **Restart the bridge** — Stop and start it again from the local `codex-to-im` workbench to pick up the new capabilities
108
+
109
+ ### Current Feishu streaming behavior with Codex runtime
110
+
111
+ Even after the Feishu permissions are correct, the current `codex` runtime does **not** guarantee token-by-token text streaming into the Feishu card.
112
+
113
+ As of **2026-03-24**, the `Codex CLI / SDK` event stream typically emits assistant text when the `agent_message` item completes, rather than as token deltas. In practice, Feishu streaming cards are best understood as:
114
+
115
+ - early `Thinking` / tool progress updates
116
+ - final response text written into the card at completion
117
+
118
+ So if you see the final answer appear all at once after the card was created successfully, that is currently expected behavior with `codex`.
210
119
 
211
120
  ### Domain (optional)
212
121
 
@@ -222,37 +131,6 @@ Leave empty to allow all users who can message the bot.
222
131
 
223
132
  ---
224
133
 
225
- ## QQ
226
-
227
- > **Note:** QQ first version only supports **C2C private chat** (sandbox access). Group chat and channel are not supported yet.
228
-
229
- ### App ID and App Secret (required)
230
-
231
- **How to get QQ Bot credentials:**
232
- 1. Go to https://q.qq.com/qqbot/openclaw
233
- 2. Log in and enter the QQ Bot / OpenClaw management page
234
- 3. Create a new QQ Bot or select an existing one
235
- 4. Find **App ID** and **App Secret** on the bot's credential page
236
- 5. Copy both values
237
-
238
- These are the only two required fields for QQ.
239
-
240
- ### Sandbox private chat setup
241
-
242
- 1. In the QQ Bot management page, configure sandbox access
243
- 2. Scan the QR code with QQ to add the bot as a friend
244
- 3. Send a message to the bot via QQ private chat to start using it
245
-
246
- ### Allowed User OpenIDs (optional)
247
-
248
- **Important:** The value is `user_openid`, NOT QQ number.
249
-
250
- `user_openid` is an opaque identifier assigned by the QQ Bot platform to each user. You can obtain it from the bot's message logs after a user sends a message to the bot.
251
-
252
- If you don't have the openid yet, leave this field empty. You can add it later via `reconfigure`.
253
-
254
- ---
255
-
256
134
  ## Weixin / 微信
257
135
 
258
136
  > Risk note: this integration follows the same OpenClaw-style WeChat plugin protocol used by CodePilot. Because it connects a non-OpenClaw product to WeChat, there may be account risk. Use with caution.
@@ -261,28 +139,28 @@ If you don't have the openid yet, leave this field empty. You can add it later v
261
139
 
262
140
  Weixin does **not** use a static bot token in `config.env`.
263
141
 
264
- Instead, run the local QR helper from the local app directory:
142
+ Instead, run the local QR helper from the local app directory:
143
+
144
+ - Repo checkout or app install:
145
+
146
+ ```bash
147
+ cd /path/to/codex-to-im
148
+ npm run weixin:login
149
+ ```
265
150
 
266
- - Repo checkout or app install:
267
-
268
- ```bash
269
- cd /path/to/codex-to-im
270
- npm run weixin:login
271
- ```
272
-
273
- If you are running from a checked-out repo, use that repo's `codex-to-im` directory.
151
+ If you are running from a checked-out repo, use that repo's `codex-to-im` directory.
274
152
 
275
153
  What happens next:
276
154
 
277
155
  1. The helper requests a fresh WeChat QR code
278
156
  2. It writes a local HTML file to:
279
- `~/.codex-to-im/runtime/weixin-login.html`
157
+ `~/.codex-to-im/runtime/weixin-login.html`
280
158
  3. It tries to open that HTML file in your default browser automatically
281
159
  4. You scan the QR code with the WeChat app and confirm on your phone
282
160
  5. On success, the helper stores the linked account in:
283
- `~/.codex-to-im/data/weixin-accounts.json`
161
+ `~/.codex-to-im/data/weixin-accounts.json`
284
162
 
285
- The filename stays plural for backward compatibility. Multiple linked Weixin accounts can coexist in the same store.
163
+ The filename stays plural for backward compatibility. Multiple linked Weixin accounts can coexist in the same store.
286
164
 
287
165
  If the browser does not open automatically, open the HTML file manually.
288
166
 
@@ -319,4 +197,4 @@ Weixin voice messages are handled differently from image/file/video media:
319
197
  - If WeChat does **not** include a transcript, the bridge returns a user-visible error asking the sender to enable WeChat voice transcription and resend.
320
198
  - The bridge does **not** download, decrypt, or transcribe raw voice audio on its own.
321
199
 
322
- This rule applies in both Claude Code and Codex runtimes.
200
+ This rule applies to the Codex runtime.