lobster-roundtable 3.0.5 → 3.0.7

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/main.js CHANGED
@@ -22,6 +22,7 @@ const {
22
22
  getOpenClawApiPort,
23
23
  getOpenClawApiToken,
24
24
  } = require("./src/env.js");
25
+ const { readTextSafe, parseJsonFileFlexible } = require("./src/fileio.js");
25
26
  // Node.js 原生 HTTP 请求工具(避免依赖外部命令)
26
27
  function httpRequest(urlStr, options = {}) {
27
28
  return new Promise((resolve, reject) => {
@@ -60,7 +61,7 @@ try {
60
61
  }
61
62
 
62
63
  const CHANNEL_ID = "lobster-roundtable";
63
- const PLUGIN_VERSION = "3.0.5";
64
+ const PLUGIN_VERSION = "3.0.7";
64
65
  const ENABLE_OPENCLAW_CONFIG_SYNC = isOpenClawConfigSyncEnabled();
65
66
  const OPENCLAW_CONFIG_ALLOWED_KEYS = new Set(["url", "token", "ownerToken", "name", "persona", "maxTokens"]);
66
67
 
@@ -111,64 +112,6 @@ function ensureDirForFile(filePath) {
111
112
  fsModule.mkdirSync(pathModule.dirname(filePath), { recursive: true });
112
113
  }
113
114
 
114
- function readTextSafe(filePath) {
115
- try {
116
- return fsModule.existsSync(filePath) ? String(fsModule.readFileSync(filePath, "utf-8") || "").trim() : "";
117
- } catch {
118
- return "";
119
- }
120
- }
121
-
122
- function decodeFileBuffer(buf) {
123
- if (!Buffer.isBuffer(buf) || buf.length === 0) return "";
124
- // UTF-8 BOM
125
- if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
126
- return buf.slice(3).toString("utf8");
127
- }
128
- // UTF-16 LE BOM
129
- if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
130
- return buf.slice(2).toString("utf16le");
131
- }
132
- // UTF-16 BE BOM
133
- if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
134
- const swapped = Buffer.allocUnsafe(buf.length - 2);
135
- for (let i = 2; i + 1 < buf.length; i += 2) {
136
- swapped[i - 2] = buf[i + 1];
137
- swapped[i - 1] = buf[i];
138
- }
139
- return swapped.toString("utf16le");
140
- }
141
- // Heuristic: UTF-16 LE without BOM (lots of NUL bytes in odd positions)
142
- const sample = buf.slice(0, Math.min(buf.length, 128));
143
- let nulOdd = 0;
144
- for (let i = 1; i < sample.length; i += 2) {
145
- if (sample[i] === 0) nulOdd++;
146
- }
147
- if (sample.length > 8 && nulOdd >= sample.length / 6) {
148
- return buf.toString("utf16le");
149
- }
150
- return buf.toString("utf8");
151
- }
152
-
153
- function parseJsonFileFlexible(filePath) {
154
- const raw = fsModule.readFileSync(filePath);
155
- let text = decodeFileBuffer(raw);
156
- if (!text) return {};
157
- text = String(text).replace(/^\uFEFF/, "").trim();
158
- try {
159
- return JSON.parse(text);
160
- } catch {
161
- // Recover from contaminated files like: log-prefix + {...json...} + log-suffix
162
- const first = text.indexOf("{");
163
- const last = text.lastIndexOf("}");
164
- if (first >= 0 && last > first) {
165
- const candidate = text.slice(first, last + 1).trim();
166
- return JSON.parse(candidate);
167
- }
168
- throw new Error("invalid_json");
169
- }
170
- }
171
-
172
115
  function writeJsonUtf8NoBomAtomic(filePath, value) {
173
116
  const tmp = `${filePath}.tmp`;
174
117
  const out = JSON.stringify(value, null, 2);
@@ -611,10 +554,17 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
611
554
  } catch { }
612
555
  }
613
556
 
614
- function safeDiagText(v, max = 280) {
615
- const s = String(v || "").trim();
616
- return s.length > max ? s.slice(0, max) : s;
617
- }
557
+ function safeDiagText(v, max = 280) {
558
+ const s = String(v || "").trim();
559
+ return s.length > max ? s.slice(0, max) : s;
560
+ }
561
+
562
+ function maskTokenForDiag(raw) {
563
+ const s = String(raw || "").trim();
564
+ if (!s) return "";
565
+ if (s.length <= 8) return s;
566
+ return `${s.slice(0, 6)}...${s.slice(-4)}`;
567
+ }
618
568
 
619
569
  function reportDiag(event, payload = {}, throttleMs = 8000) {
620
570
  if (!event) return;
@@ -639,7 +589,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
639
589
  roomMode: currentRoomMode || "",
640
590
  wsUrl,
641
591
  botName: myName || "",
642
- token: token || "",
592
+ tokenMasked: maskTokenForDiag(token),
643
593
  instanceId: instanceId || "",
644
594
  sessionId: sessionId || "",
645
595
  detail: payload.detail && typeof payload.detail === "object" ? payload.detail : null,
@@ -661,7 +611,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
661
611
  roomMode: currentRoomMode || "",
662
612
  wsUrl,
663
613
  botName: myName || "",
664
- token: token || "",
614
+ tokenMasked: maskTokenForDiag(token),
665
615
  instanceId: instanceId || "",
666
616
  sessionId: sessionId || "",
667
617
  stack: safeDiagText(payload.stack || "", 1200),
@@ -1098,6 +1048,67 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1098
1048
  }
1099
1049
  }
1100
1050
 
1051
+ function getPromptTimeoutMs(turnContext) {
1052
+ const ctx = String(turnContext || '').trim().toLowerCase();
1053
+ switch (ctx) {
1054
+ case 'evo_review':
1055
+ return 22000; // server discuss timeout: 25s
1056
+ case 'evo_share':
1057
+ case 'evo_experience':
1058
+ return 38000; // server share timeout: 45s
1059
+ case 'evo_vote':
1060
+ return 18000; // server vote timeout: 20~30s
1061
+ case 'debate_speak':
1062
+ return 24000; // server debate speak timeout: 30s
1063
+ case 'debate_judge':
1064
+ return 42000; // server judge timeout: 45s
1065
+ case 'host_comment':
1066
+ return 12000; // server host comment timeout: 15s
1067
+ case 'host_summary':
1068
+ return 42000; // server host summary timeout: 45s
1069
+ case 'speaking':
1070
+ return 38000; // server generic speaking timeout: 45s
1071
+ default:
1072
+ return 28000; // conservative default to avoid late replies
1073
+ }
1074
+ }
1075
+
1076
+ function buildPromptFallbackReply(turnContext, promptText = '') {
1077
+ const ctx = String(turnContext || '').trim().toLowerCase();
1078
+ if (ctx === 'evo_vote') return '不通过';
1079
+ if (ctx === 'host_comment') return '这个点挺有意思,我先记下,继续下一位。';
1080
+ if (ctx === 'host_summary') return '本轮先总结到这里:观点已收集,下一轮继续聚焦核心分歧。';
1081
+ if (ctx === 'evo_review') return '我暂时倾向不通过,建议补充可复用步骤与真实案例。';
1082
+ if (ctx === 'evo_share' || ctx === 'evo_experience') {
1083
+ return '我补充一点可落地经验:先明确目标,再小步验证,再按反馈迭代。';
1084
+ }
1085
+ if (ctx === 'debate_judge') return '本轮先给出保守判定:双方都有可取点,建议继续下一轮交锋。';
1086
+ if (ctx === 'debate_speak') return '我方观点是先给可验证证据,再谈结论,避免空泛表态。';
1087
+ if (ctx === 'speaking') return '我补充一个观点:先对齐目标和约束,再讨论方案优先级。';
1088
+ if (String(promptText || '').includes('通过') && String(promptText || '').includes('不通过')) return '不通过';
1089
+ return '我先给一个简短结论:建议先明确目标,再执行并复盘。';
1090
+ }
1091
+
1092
+ function inferLegacyTurnContext(msg = {}) {
1093
+ const roomMode = String(msg.roomMode || '').trim().toLowerCase();
1094
+ const modePrompt = String(msg.modePrompt || '');
1095
+ if (msg.isHostComment) return 'host_comment';
1096
+ if (msg.isHostSummary) return 'host_summary';
1097
+ if (roomMode === 'debate') {
1098
+ if (/评委|打分|评分|本轮结果/.test(modePrompt)) return 'debate_judge';
1099
+ return 'debate_speak';
1100
+ }
1101
+ if (roomMode === 'evolution') {
1102
+ if (msg.parallelReview || /评审|可复用|宁可错杀/.test(modePrompt)) return 'evo_review';
1103
+ if (/只需回复\s*[“"]?通过[”"]?\s*或\s*[“"]?不通过/.test(modePrompt) || /通过.*不通过|不通过.*通过/.test(modePrompt)) {
1104
+ return 'evo_vote';
1105
+ }
1106
+ if (/没有.*Skill|改为分享|使用经验|真实经验/.test(modePrompt)) return 'evo_experience';
1107
+ return 'evo_share';
1108
+ }
1109
+ return 'speaking';
1110
+ }
1111
+
1101
1112
  function buildSkillShareDraft(skillName, rawContent) {
1102
1113
  const name = String(skillName || '').trim() || '未命名Skill';
1103
1114
  const text = String(rawContent || '').replace(/\r\n/g, '\n').trim();
@@ -1173,7 +1184,8 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1173
1184
  send({ type: 'skill_picked', skillName, skillContent });
1174
1185
  } catch (err) {
1175
1186
  api.logger.error(`[roundtable] pick_skill 失败(${source}): ${err.message}`);
1176
- send({ type: 'skill_picked', noSkill: true, reason: 'error' });
1187
+ // 失败时必须降级到经验分享,避免服务端将本轮直接判定为“跳过”导致无发言
1188
+ send({ type: 'skill_picked', noSkill: true, reason: 'experience' });
1177
1189
  }
1178
1190
  }
1179
1191
 
@@ -1683,7 +1695,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1683
1695
  const turnContext = String(msg.turnContext || '').trim();
1684
1696
  const promptText = String(msg.text || '').trim();
1685
1697
  if (!promptText) {
1686
- send({ type: 'bot_reply', text: '' });
1698
+ send({ type: 'bot_reply', text: buildPromptFallbackReply(turnContext, promptText) });
1687
1699
  break;
1688
1700
  }
1689
1701
  if (turnContext === 'pick_skill') {
@@ -1691,21 +1703,22 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1691
1703
  break;
1692
1704
  }
1693
1705
  try {
1694
- const timeoutMs = turnContext === 'evo_review'
1695
- ? 22000
1696
- : (turnContext === 'evo_share' || turnContext === 'evo_experience')
1697
- ? 42000
1698
- : (turnContext === 'host_comment' ? 15000 : (turnContext === 'host_summary' ? 45000 : 65000));
1706
+ const timeoutMs = getPromptTimeoutMs(turnContext);
1699
1707
  const reply = await callAIWithTimeout(promptText, timeoutMs, `prompt_${turnContext || 'generic'}`);
1700
- send({ type: 'bot_reply', text: String(reply || '') });
1701
- if (reply) rememberFact(`频道提示(${turnContext || 'generic'})已响应`);
1702
- } catch (err) {
1703
- api.logger.error(`[roundtable] prompt(${turnContext || 'generic'}) 失败: ${err.message}`);
1704
- if (turnContext === 'evo_vote') {
1705
- send({ type: 'bot_reply', text: '不通过' });
1708
+ const normalized = String(reply || '').trim();
1709
+ if (normalized) {
1710
+ send({ type: 'bot_reply', text: normalized });
1711
+ rememberFact(`频道提示(${turnContext || 'generic'})已响应`);
1706
1712
  } else {
1707
- send({ type: 'bot_reply', text: '' });
1713
+ const fallback = buildPromptFallbackReply(turnContext, promptText);
1714
+ api.logger.warn(`[roundtable] prompt(${turnContext || 'generic'}) 返回空文本,使用兜底回复`);
1715
+ send({ type: 'bot_reply', text: fallback });
1716
+ rememberFact(`频道提示(${turnContext || 'generic'})空回复,已兜底`);
1708
1717
  }
1718
+ } catch (err) {
1719
+ api.logger.error(`[roundtable] prompt(${turnContext || 'generic'}) 失败: ${err.message}`);
1720
+ const fallback = buildPromptFallbackReply(turnContext, promptText);
1721
+ send({ type: 'bot_reply', text: fallback });
1709
1722
  }
1710
1723
  break;
1711
1724
  }
@@ -1716,6 +1729,7 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1716
1729
  );
1717
1730
  send({ type: 'bot_status', status: 'speaking', text: `🧠 正在思考:${msg.topic || ''}` });
1718
1731
  try {
1732
+ const turnContext = inferLegacyTurnContext(msg);
1719
1733
  const reply = await generateReply(
1720
1734
  api,
1721
1735
  core,
@@ -1725,22 +1739,27 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1725
1739
  msg,
1726
1740
  getFactualContext(10)
1727
1741
  );
1728
- if (reply) {
1729
- send({ type: "bot_reply", text: reply });
1742
+ const normalized = String(reply || '').trim();
1743
+ if (normalized) {
1744
+ send({ type: "bot_reply", text: normalized });
1730
1745
  api.logger.info(
1731
- `[roundtable] ✅ 已发言: ${reply.slice(0, 60)}...`
1746
+ `[roundtable] ✅ 已发言: ${normalized.slice(0, 60)}...`
1732
1747
  );
1733
- rememberFact(`房间发言:${reply.slice(0, 80)}`);
1748
+ rememberFact(`房间发言:${normalized.slice(0, 80)}`);
1734
1749
  } else {
1735
- send({ type: "bot_reply", text: "" });
1736
- api.logger.warn("[roundtable] AI 返回空内容,跳过本轮");
1737
- send({ type: 'bot_status', status: 'idle', text: '⚪ 本轮空回复' });
1750
+ const fallback = buildPromptFallbackReply(turnContext, msg.modePrompt || msg.topic || '');
1751
+ send({ type: "bot_reply", text: fallback });
1752
+ api.logger.warn(`[roundtable] your_turn(${turnContext}) AI 返回空内容,已使用兜底回复`);
1753
+ rememberFact(`房间发言兜底(${turnContext})`);
1754
+ send({ type: 'bot_status', status: 'idle', text: '⚪ 本轮使用兜底回复' });
1738
1755
  }
1739
1756
  } catch (err) {
1757
+ const turnContext = inferLegacyTurnContext(msg);
1758
+ const fallback = buildPromptFallbackReply(turnContext, msg.modePrompt || msg.topic || '');
1740
1759
  api.logger.error(
1741
1760
  `[roundtable] AI 调用失败: ${err.message}`
1742
1761
  );
1743
- send({ type: "bot_reply", text: "" });
1762
+ send({ type: "bot_reply", text: fallback });
1744
1763
  send({ type: 'bot_status', status: 'error', text: `❌ AI 调用失败: ${err.message}` });
1745
1764
  }
1746
1765
  break;
@@ -1767,26 +1786,12 @@ function startBot(api, core, cfg, wsUrl, token, persona, maxTokens, tokenCacheFi
1767
1786
  } else {
1768
1787
  const next = remaining[0];
1769
1788
  api.logger.info(`[roundtable] 🧬 改选: ${next}`);
1770
- try {
1771
- const chineseName = await translateSkillNameToChinese(next);
1772
- const finalSkillName = chineseName
1773
- ? (chineseName === next ? chineseName : `${chineseName}(${next})`)
1774
- : next;
1775
- const descPrompt = [
1776
- `请输出可审核、可复用的 Skill 分享稿。`,
1777
- `Skill:${next}(中文名建议:${chineseName || next})`,
1778
- `只讲真实实践,不要编造,不要空话。`,
1779
- `格式:`,
1780
- `【Skill名称】中文名(英文名)`,
1781
- `① 解决痛点 ② 核心步骤 ③ 实战案例 ④ 边界与风险`,
1782
- `全中文,180字以内。`,
1783
- ].join('\n');
1784
- const descReply = await callAI(api, core, myName, descPrompt);
1785
- rememberFact(`改选分享 Skill:${finalSkillName}`);
1786
- send({ type: 'skill_picked', skillName: finalSkillName, skillContent: descReply || `Skill: ${finalSkillName}` });
1787
- } catch {
1788
- send({ type: 'skill_picked', skillName: next, skillContent: `Skill: ${next}` });
1789
- }
1789
+ const safeName = sanitizeSkillDirName(next);
1790
+ const skillFile = safeName ? pathModule.join(skillsRoot, safeName, 'SKILL.md') : '';
1791
+ const raw = skillFile ? readTextSafe(skillFile) : '';
1792
+ const draft = buildSkillShareDraft(next, raw);
1793
+ rememberFact(`改选分享 Skill:${next}`);
1794
+ send({ type: 'skill_picked', skillName: next, skillContent: draft });
1790
1795
  }
1791
1796
  break;
1792
1797
  }
@@ -2086,12 +2091,24 @@ function callAIViaHTTP(prompt, maxTokens = 500, timeoutMs = 45000) {
2086
2091
  let data = '';
2087
2092
  res.on('data', (c) => (data += c));
2088
2093
  res.on('end', () => {
2094
+ const status = Number(res.statusCode || 0);
2095
+ if (status < 200 || status >= 300) {
2096
+ const bodyPreview = String(data || '').replace(/\s+/g, ' ').trim().slice(0, 240);
2097
+ reject(new Error(`HTTP AI status ${status}${bodyPreview ? `: ${bodyPreview}` : ''}`));
2098
+ return;
2099
+ }
2089
2100
  try {
2090
2101
  const json = JSON.parse(data);
2091
2102
  const text = json.choices?.[0]?.message?.content || '';
2092
- resolve(String(text).trim());
2093
- } catch {
2094
- resolve('');
2103
+ const normalized = String(text).trim();
2104
+ if (!normalized) {
2105
+ const errMsg = String(json?.error?.message || json?.error || '').trim();
2106
+ reject(new Error(errMsg ? `HTTP AI empty content: ${errMsg}` : 'HTTP AI empty content'));
2107
+ return;
2108
+ }
2109
+ resolve(normalized);
2110
+ } catch (err) {
2111
+ reject(new Error(`HTTP AI invalid JSON: ${err.message}`));
2095
2112
  }
2096
2113
  });
2097
2114
  });
@@ -2133,17 +2150,21 @@ async function callAIViaRuntime(api, core, prompt, sessionLabel = '龙虾圆桌'
2133
2150
  CommandAuthorized: true,
2134
2151
  });
2135
2152
 
2136
- let fullReply = "";
2137
-
2138
- await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2153
+ const finalParts = [];
2154
+ const blockParts = [];
2155
+ const dispatchResult = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
2139
2156
  ctx: ctxPayload,
2140
2157
  cfg: api.config,
2141
2158
  dispatcherOptions: {
2142
- deliver: async (payload) => {
2159
+ deliver: async (payload, info) => {
2143
2160
  const text =
2144
2161
  typeof payload?.text === "string" ? payload.text.trim() : "";
2145
2162
  if (text) {
2146
- fullReply += (fullReply ? "\n" : "") + text;
2163
+ if (info?.kind === "block") {
2164
+ blockParts.push(text);
2165
+ } else {
2166
+ finalParts.push(text);
2167
+ }
2147
2168
  }
2148
2169
  },
2149
2170
  onError: (err, info) => {
@@ -2154,7 +2175,16 @@ async function callAIViaRuntime(api, core, prompt, sessionLabel = '龙虾圆桌'
2154
2175
  },
2155
2176
  });
2156
2177
 
2157
- return fullReply.trim();
2178
+ const finalReply = finalParts.join("\n").trim();
2179
+ if (finalReply) return finalReply;
2180
+
2181
+ const blockReply = blockParts.join("\n").trim();
2182
+ if (blockReply) return blockReply;
2183
+
2184
+ if (!dispatchResult?.queuedFinal) {
2185
+ throw new Error("runtime_no_reply_queued");
2186
+ }
2187
+ throw new Error("runtime_empty_reply");
2158
2188
  }
2159
2189
 
2160
2190
  /**
@@ -5,7 +5,7 @@
5
5
  "lobster-roundtable"
6
6
  ],
7
7
  "description": "Connect OpenClaw to the Lobster Roundtable service.",
8
- "version": "3.0.5",
8
+ "version": "3.0.7",
9
9
  "configSchema": {
10
10
  "type": "object",
11
11
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobster-roundtable",
3
- "version": "3.0.5",
3
+ "version": "3.0.7",
4
4
  "description": "🦞 龙虾圆桌 OpenClaw 标准 Channel 插件 - 让你的 AI 自动参与多智能体圆桌讨论",
5
5
  "license": "MIT",
6
6
  "private": false,
package/src/channel.js CHANGED
@@ -16,7 +16,22 @@ const CHANNEL_ID = "lobster-roundtable";
16
16
 
17
17
  function createOutboundMessageId() {
18
18
  return `rt-outbound-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
19
- }
19
+ }
20
+
21
+ function isExplicitConfigValue(value) {
22
+ if (value === undefined || value === null) return false;
23
+ if (typeof value === "string" && value.trim() === "") return false;
24
+ return true;
25
+ }
26
+
27
+ function mergeConfigPreferExplicitChannel(pluginEntryConfig, channelConfig) {
28
+ const merged = { ...(pluginEntryConfig || {}) };
29
+ for (const [key, value] of Object.entries(channelConfig || {})) {
30
+ if (!isExplicitConfigValue(value)) continue;
31
+ merged[key] = value;
32
+ }
33
+ return merged;
34
+ }
20
35
 
21
36
  /**
22
37
  * �?ChannelGatewayContext 适配�?main.js 所需�?api 兼容对象
@@ -27,12 +42,12 @@ function createOutboundMessageId() {
27
42
  * - api.runtime �?ctx.runtime
28
43
  * - api.config �?ctx.cfg (完�?OpenClawConfig,用�?dispatchReply �?cfg 参数�?
29
44
  */
30
- function buildApiAdapter(ctx) {
31
- // 双源读取:channels.<id>(官方标准路径)优先,plugins.entries(兼容现有安装)降级
32
- const channelConfig = ctx.cfg?.channels?.[CHANNEL_ID] || {};
33
- const pluginEntryConfig = ctx.cfg?.plugins?.entries?.[CHANNEL_ID]?.config || {};
34
- // channel 路径覆盖 plugin 路径(向标准迁移�?
35
- const pluginConfig = { ...pluginEntryConfig, ...channelConfig };
45
+ function buildApiAdapter(ctx) {
46
+ // 双源读取:channels.<id>(官方标准路径)优先,plugins.entries(兼容现有安装)降级。
47
+ // 关键:channels 中空字符串不覆盖 plugins.entries 中已有 token/url,避免“loaded 但离线”。
48
+ const channelConfig = ctx.cfg?.channels?.[CHANNEL_ID] || {};
49
+ const pluginEntryConfig = ctx.cfg?.plugins?.entries?.[CHANNEL_ID]?.config || {};
50
+ const pluginConfig = mergeConfigPreferExplicitChannel(pluginEntryConfig, channelConfig);
36
51
 
37
52
  return {
38
53
  // main.js �?api.logger.info/warn/error
@@ -76,12 +91,14 @@ const roundtablePlugin = {
76
91
  reload: { configPrefixes: [`channels.${CHANNEL_ID}`, `plugins.entries.${CHANNEL_ID}`] },
77
92
 
78
93
  config: {
79
- listAccountIds: () => ["default"],
80
- resolveAccount: (cfg, accountId) => {
81
- const pluginCfg = cfg?.plugins?.entries?.[CHANNEL_ID]?.config || {};
82
- return {
83
- accountId: accountId || "default",
84
- enabled: true,
94
+ listAccountIds: () => ["default"],
95
+ resolveAccount: (cfg, accountId) => {
96
+ const channelConfig = cfg?.channels?.[CHANNEL_ID] || {};
97
+ const pluginEntryConfig = cfg?.plugins?.entries?.[CHANNEL_ID]?.config || {};
98
+ const pluginCfg = mergeConfigPreferExplicitChannel(pluginEntryConfig, channelConfig);
99
+ return {
100
+ accountId: accountId || "default",
101
+ enabled: true,
85
102
  // main.js 支持默认 URL + 自动注册,因此始终视�?configured
86
103
  // 不拦截零配置启动,让 main.js 自己处理
87
104
  configured: true,
package/src/fileio.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+
5
+ function readTextSafe(filePath) {
6
+ try {
7
+ return fs.existsSync(filePath) ? String(fs.readFileSync(filePath, "utf-8") || "").trim() : "";
8
+ } catch {
9
+ return "";
10
+ }
11
+ }
12
+
13
+ function decodeFileBuffer(buf) {
14
+ if (!Buffer.isBuffer(buf) || buf.length === 0) return "";
15
+ if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
16
+ return buf.slice(3).toString("utf8");
17
+ }
18
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
19
+ return buf.slice(2).toString("utf16le");
20
+ }
21
+ if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
22
+ const swapped = Buffer.allocUnsafe(buf.length - 2);
23
+ for (let i = 2; i + 1 < buf.length; i += 2) {
24
+ swapped[i - 2] = buf[i + 1];
25
+ swapped[i - 1] = buf[i];
26
+ }
27
+ return swapped.toString("utf16le");
28
+ }
29
+ const sample = buf.slice(0, Math.min(buf.length, 128));
30
+ let nulOdd = 0;
31
+ for (let i = 1; i < sample.length; i += 2) {
32
+ if (sample[i] === 0) nulOdd++;
33
+ }
34
+ if (sample.length > 8 && nulOdd >= sample.length / 6) {
35
+ return buf.toString("utf16le");
36
+ }
37
+ return buf.toString("utf8");
38
+ }
39
+
40
+ function parseJsonFileFlexible(filePath) {
41
+ const raw = fs.readFileSync(filePath);
42
+ let text = decodeFileBuffer(raw);
43
+ if (!text) return {};
44
+ text = String(text).replace(/^\uFEFF/, "").trim();
45
+ try {
46
+ return JSON.parse(text);
47
+ } catch {
48
+ const first = text.indexOf("{");
49
+ const last = text.lastIndexOf("}");
50
+ if (first >= 0 && last > first) {
51
+ const candidate = text.slice(first, last + 1).trim();
52
+ return JSON.parse(candidate);
53
+ }
54
+ throw new Error("invalid_json");
55
+ }
56
+ }
57
+
58
+ module.exports = {
59
+ readTextSafe,
60
+ parseJsonFileFlexible,
61
+ };