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.
- package/dist/cli.mjs +37 -9
- package/dist/daemon.mjs +3091 -9102
- package/dist/ui-server.mjs +113 -63
- package/package.json +2 -4
- package/references/setup-guides.md +34 -156
- package/references/token-validation.md +28 -44
- package/references/troubleshooting.md +10 -11
- package/scripts/build.js +2 -7
- package/scripts/daemon.sh +11 -30
- package/scripts/doctor.sh +35 -280
- package/scripts/supervisor-macos.sh +9 -28
package/dist/ui-server.mjs
CHANGED
|
@@ -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:
|
|
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
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
\
|
|
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
|
-
\
|
|
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\
|
|
9610
|
-
<div class="small">\u5F53\u524D\u9700\u8981\u91CD\u542F Bridge \u7684\u914D\u7F6E\uFF1A<code>Runtime</code>\u3001<code>\
|
|
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: '\
|
|
10109
|
-
streamStatusCheckIntervalSeconds: '\
|
|
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.
|
|
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.
|
|
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
|
-
-
|
|
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
|
|
200
|
+
This rule applies to the Codex runtime.
|