@unlinearity/cli-wechat-bridge 1.0.4 → 1.0.6

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.
@@ -7,22 +7,79 @@ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
7
7
  const PROJECT_DIR = path.resolve(MODULE_DIR, "..", "..");
8
8
  export const DEFAULT_BASE_URL = process.env.WECHAT_ILINK_BASE_URL?.trim() || "https://ilinkai.weixin.qq.com";
9
9
  export const BOT_TYPE = "3";
10
- export const CHANNEL_DATA_DIR = process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR?.trim()
11
- ? path.resolve(process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR.trim())
12
- : path.join(os.homedir(), ".claude", "channels", "wechat");
13
- export const GLOBAL_CHANNEL_DATA_DIR = path.join(os.homedir(), ".claude", "channels", "wechat");
10
+ export function resolveChannelDataDir(env = process.env, homeDir = os.homedir()) {
11
+ const configured = env.CLI_BRIDGE_DATA_DIR?.trim();
12
+ return configured ? path.resolve(configured) : path.join(homeDir, ".cli-bridge");
13
+ }
14
+ export const CHANNEL_DATA_DIR = resolveChannelDataDir();
14
15
  export const CREDENTIALS_FILE = path.join(CHANNEL_DATA_DIR, "account.json");
15
16
  export const SYNC_BUF_FILE = path.join(CHANNEL_DATA_DIR, "sync_buf.txt");
16
17
  export const CONTEXT_CACHE_FILE = path.join(CHANNEL_DATA_DIR, "context_tokens.json");
17
18
  export const BRIDGE_STATE_FILE = path.join(CHANNEL_DATA_DIR, "bridge-state.json");
18
19
  export const BRIDGE_LOG_FILE = path.join(CHANNEL_DATA_DIR, "bridge.log");
19
20
  export const BRIDGE_LOCK_FILE = path.join(CHANNEL_DATA_DIR, "bridge.lock.json");
21
+ export const DAEMON_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "daemon-endpoint.json");
20
22
  export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-panel-endpoint.json");
21
23
  export const WORKSPACES_DIR = path.join(CHANNEL_DATA_DIR, "workspaces");
22
- export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(GLOBAL_CHANNEL_DATA_DIR, "inbound-message-claims");
23
- const LEGACY_CHANNEL_DATA_DIR = path.join(PROJECT_DIR, "~", ".claude", "channels", "wechat");
24
- const LEGACY_CREDENTIALS_FILE = path.join(LEGACY_CHANNEL_DATA_DIR, "account.json");
25
- const LEGACY_SYNC_BUF_FILE = path.join(LEGACY_CHANNEL_DATA_DIR, "sync_buf.txt");
24
+ export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-message-claims");
25
+ export const INBOUND_ATTACHMENTS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-attachments");
26
+ const LEGACY_GLOBAL_CHANNEL_DATA_DIR = path.join(os.homedir(), ".claude", "channels", "wechat");
27
+ const LEGACY_REPO_CHANNEL_DATA_DIR = path.join(PROJECT_DIR, "~", ".claude", "channels", "wechat");
28
+ const LEGACY_ENV_CHANNEL_DATA_DIR = process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR?.trim()
29
+ ? path.resolve(process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR.trim())
30
+ : "";
31
+ const LEGACY_CHANNEL_SOURCE_DIRS = [
32
+ LEGACY_ENV_CHANNEL_DATA_DIR,
33
+ LEGACY_GLOBAL_CHANNEL_DATA_DIR,
34
+ LEGACY_REPO_CHANNEL_DATA_DIR,
35
+ ].filter(Boolean);
36
+ const LEGACY_CHANNEL_SOURCES = LEGACY_CHANNEL_SOURCE_DIRS.map((dataDir) => ({
37
+ dataDir,
38
+ }));
39
+ const LEGACY_MIGRATION_ITEMS = [
40
+ {
41
+ label: "credentials",
42
+ sourceName: "account.json",
43
+ targetName: "account.json",
44
+ kind: "file",
45
+ },
46
+ {
47
+ label: "sync state",
48
+ sourceName: "sync_buf.txt",
49
+ targetName: "sync_buf.txt",
50
+ kind: "file",
51
+ },
52
+ {
53
+ label: "context tokens",
54
+ sourceName: "context_tokens.json",
55
+ targetName: "context_tokens.json",
56
+ kind: "file",
57
+ },
58
+ {
59
+ label: "update check cache",
60
+ sourceName: "update-check.json",
61
+ targetName: "update-check.json",
62
+ kind: "file",
63
+ },
64
+ {
65
+ label: "workspace state",
66
+ sourceName: "workspaces",
67
+ targetName: "workspaces",
68
+ kind: "directory",
69
+ },
70
+ {
71
+ label: "inbound attachments",
72
+ sourceName: "inbound-attachments",
73
+ targetName: "inbound-attachments",
74
+ kind: "directory",
75
+ },
76
+ {
77
+ label: "legacy bridge log",
78
+ sourceName: "bridge.log",
79
+ targetName: "legacy-bridge.log",
80
+ kind: "file",
81
+ },
82
+ ];
26
83
  export function ensureChannelDataDir() {
27
84
  fs.mkdirSync(CHANNEL_DATA_DIR, { recursive: true });
28
85
  }
@@ -58,46 +115,82 @@ export function getWorkspaceChannelPaths(cwd) {
58
115
  endpointFile: path.join(workspaceDir, "codex-panel-endpoint.json"),
59
116
  };
60
117
  }
118
+ export function getWorkspaceAdapterEndpointFile(cwd, adapter) {
119
+ return path.join(getWorkspaceChannelPaths(cwd).workspaceDir, `${adapter}-companion-endpoint.json`);
120
+ }
61
121
  export function ensureWorkspaceChannelDir(cwd) {
62
122
  ensureChannelDataDir();
63
123
  const paths = getWorkspaceChannelPaths(cwd);
64
124
  fs.mkdirSync(paths.workspaceDir, { recursive: true });
65
125
  return paths;
66
126
  }
67
- function channelDataDirHasState() {
68
- try {
69
- if (!fs.existsSync(CHANNEL_DATA_DIR)) {
127
+ function isSamePath(left, right) {
128
+ const normalizedLeft = path.resolve(left);
129
+ const normalizedRight = path.resolve(right);
130
+ return process.platform === "win32"
131
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
132
+ : normalizedLeft === normalizedRight;
133
+ }
134
+ function legacySourceHasMigratableData(source) {
135
+ return LEGACY_MIGRATION_ITEMS.some((item) => {
136
+ const sourcePath = path.join(source.dataDir, item.sourceName);
137
+ if (!fs.existsSync(sourcePath)) {
70
138
  return false;
71
139
  }
72
- return fs.readdirSync(CHANNEL_DATA_DIR).length > 0;
73
- }
74
- catch {
75
- return true;
76
- }
140
+ try {
141
+ const stat = fs.statSync(sourcePath);
142
+ return item.kind === "directory" ? stat.isDirectory() : stat.isFile();
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ });
77
148
  }
78
- export function shouldMigrateLegacyCredentials(params) {
79
- return (params.legacyCredentialsExists &&
80
- !params.credentialsExists &&
81
- !params.channelDataDirHadState);
149
+ function findLegacyChannelSource(channelDataDir = CHANNEL_DATA_DIR, legacySources = LEGACY_CHANNEL_SOURCES) {
150
+ return (legacySources.find((source) => !isSamePath(source.dataDir, channelDataDir) &&
151
+ legacySourceHasMigratableData(source)) ?? null);
82
152
  }
83
- export function migrateLegacyChannelFiles(log) {
153
+ export function migrateLegacyChannelFiles(log, options = {}) {
154
+ const channelDataDir = options.channelDataDir ?? CHANNEL_DATA_DIR;
155
+ const legacySources = (options.legacyDataDirs ?? LEGACY_CHANNEL_SOURCE_DIRS).map((dataDir) => ({ dataDir }));
84
156
  const migrated = [];
85
- const channelDataDirHadState = channelDataDirHasState();
86
- if (!fs.existsSync(LEGACY_CREDENTIALS_FILE) &&
87
- !fs.existsSync(LEGACY_SYNC_BUF_FILE)) {
157
+ const skippedExisting = [];
158
+ const legacySource = findLegacyChannelSource(channelDataDir, legacySources);
159
+ if (!legacySource) {
88
160
  return migrated;
89
161
  }
90
- ensureChannelDataDir();
91
- if (shouldMigrateLegacyCredentials({
92
- channelDataDirHadState,
93
- credentialsExists: fs.existsSync(CREDENTIALS_FILE),
94
- legacyCredentialsExists: fs.existsSync(LEGACY_CREDENTIALS_FILE),
95
- })) {
96
- fs.copyFileSync(LEGACY_CREDENTIALS_FILE, CREDENTIALS_FILE);
97
- migrated.push("credentials");
162
+ fs.mkdirSync(channelDataDir, { recursive: true });
163
+ for (const item of LEGACY_MIGRATION_ITEMS) {
164
+ const sourcePath = path.join(legacySource.dataDir, item.sourceName);
165
+ const targetPath = path.join(channelDataDir, item.targetName);
166
+ if (!fs.existsSync(sourcePath)) {
167
+ continue;
168
+ }
169
+ if (fs.existsSync(targetPath)) {
170
+ skippedExisting.push(item.label);
171
+ continue;
172
+ }
173
+ const stat = fs.statSync(sourcePath);
174
+ if (item.kind === "directory") {
175
+ if (!stat.isDirectory()) {
176
+ continue;
177
+ }
178
+ fs.cpSync(sourcePath, targetPath, { recursive: true });
179
+ }
180
+ else {
181
+ if (!stat.isFile()) {
182
+ continue;
183
+ }
184
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
185
+ fs.copyFileSync(sourcePath, targetPath);
186
+ }
187
+ migrated.push(item.label);
98
188
  }
99
189
  if (migrated.length && log) {
100
- log(`Migrated legacy ${migrated.join(" and ")} from ${LEGACY_CHANNEL_DATA_DIR} to ${CHANNEL_DATA_DIR}.`);
190
+ const skippedText = skippedExisting.length
191
+ ? ` Skipped existing: ${skippedExisting.join(", ")}.`
192
+ : "";
193
+ log(`Migrated legacy ${migrated.join(", ")} from ${legacySource.dataDir} to ${channelDataDir}.${skippedText}`);
101
194
  }
102
195
  return migrated;
103
196
  }
@@ -8,15 +8,13 @@
8
8
  * Usage:
9
9
  * bun run src/wechat/standalone-bot.ts
10
10
  */
11
- import fs from "node:fs";
12
- import os from "node:os";
13
11
  import crypto from "node:crypto";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
14
  import Anthropic from "@anthropic-ai/sdk";
15
+ import { CHANNEL_DATA_DIR, CREDENTIALS_FILE, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
15
16
  // Constants
16
- const HOME_DIR = os.homedir();
17
- const CREDENTIALS_FILE = `${HOME_DIR}/.claude/channels/wechat/account.json`;
18
- const SYNC_BUF_FILE = `${HOME_DIR}/.claude/channels/wechat/sync_buf.txt`;
19
- const LOG_DIR = `${HOME_DIR}/.claude/channels/wechat/logs`;
17
+ const LOG_DIR = path.join(CHANNEL_DATA_DIR, "logs");
20
18
  const BOT_TYPE = "3";
21
19
  const CHANNEL_VERSION = "0.1.0";
22
20
  const LONG_POLL_TIMEOUT_MS = 35_000;
@@ -329,6 +327,7 @@ async function startPolling(account) {
329
327
  async function main() {
330
328
  log("=== WeChat + Claude 独立机器人 ===");
331
329
  log("");
330
+ migrateLegacyChannelFiles(log);
332
331
  // Load WeChat credentials
333
332
  log(`正在加载微信凭据: ${CREDENTIALS_FILE}`);
334
333
  const account = loadCredentials();
@@ -8,7 +8,7 @@
8
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
10
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
- import { WeChatTransport, } from "./wechat-transport.js";
11
+ import { formatByteSize, WeChatTransport, } from "./wechat-transport.js";
12
12
  const SERVER_NAME = "wechat";
13
13
  const SERVER_VERSION = "0.3.0";
14
14
  const DEFAULT_POLL_TIMEOUT_MS = 1_000;
@@ -176,15 +176,26 @@ async function fetchMessages(args) {
176
176
  return formatFetchedMessages(result.messages);
177
177
  }
178
178
  function formatFetchedMessages(messages) {
179
- const blocks = messages.map((message, index) => [
180
- `[${index + 1}]`,
181
- `sender_id: ${message.senderId}`,
182
- `sender: ${message.sender}`,
183
- `session_id: ${message.sessionId || "(unknown)"}`,
184
- `created_at: ${message.createdAt}`,
185
- "text:",
186
- message.text,
187
- ].join("\n"));
179
+ const blocks = messages.map((message, index) => {
180
+ const lines = [
181
+ `[${index + 1}]`,
182
+ `sender_id: ${message.senderId}`,
183
+ `sender: ${message.sender}`,
184
+ `session_id: ${message.sessionId || "(unknown)"}`,
185
+ `created_at: ${message.createdAt}`,
186
+ "text:",
187
+ message.text,
188
+ ];
189
+ if (message.attachments.length > 0) {
190
+ lines.push("attachments:", ...message.attachments.map((attachment) => [
191
+ `- kind: ${attachment.kind}`,
192
+ ` name: ${attachment.fileName}`,
193
+ ` size: ${formatByteSize(attachment.sizeBytes)}`,
194
+ ` path: ${attachment.path}`,
195
+ ].join("\n")));
196
+ }
197
+ return lines.join("\n");
198
+ });
188
199
  return [
189
200
  `Fetched ${messages.length} new WeChat message${messages.length === 1 ? "" : "s"}.`,
190
201
  "",