@bastdewfn/cc-remote 1.0.9
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/1.jpg +0 -0
- package/2.jpg +0 -0
- package/README.md +150 -0
- package/bin/cc-remote.js +183 -0
- package/commands/cc-remote-close.md +5 -0
- package/commands/cc-remote-doctor.md +20 -0
- package/commands/cc-remote-open.md +5 -0
- package/config.example.json +10 -0
- package/dist/cli.js +313 -0
- package/dist/commands.js +122 -0
- package/dist/config-setup.js +366 -0
- package/dist/core.js +453 -0
- package/dist/engine/events.js +78 -0
- package/dist/feishu/cards/base.js +114 -0
- package/dist/feishu/cards/help.js +33 -0
- package/dist/feishu/cards/live.js +65 -0
- package/dist/feishu/cards/session.js +59 -0
- package/dist/feishu/cards/status.js +60 -0
- package/dist/feishu/client.js +174 -0
- package/dist/feishu/replier.js +143 -0
- package/dist/feishu/router.js +61 -0
- package/dist/feishu-bot.js +139 -0
- package/dist/feishu-mode.js +62 -0
- package/dist/index.js +62 -0
- package/dist/main.js +397 -0
- package/dist/port.js +79 -0
- package/dist/preload.js +41 -0
- package/dist/pty.js +23 -0
- package/dist/relay.js +851 -0
- package/dist/renderer.js +318 -0
- package/dist/weixin/api.js +328 -0
- package/dist/weixin/client.js +136 -0
- package/dist/weixin/replier.js +73 -0
- package/dist/weixin/types.js +10 -0
- package/index.html +32 -0
- package/package.json +85 -0
- package/scripts/patch-7za.js +94 -0
package/dist/core.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.safeTools = void 0;
|
|
37
|
+
exports.loadConfig = loadConfig;
|
|
38
|
+
exports.getDataDir = getDataDir;
|
|
39
|
+
exports.dataPath = dataPath;
|
|
40
|
+
exports.dataPathFor = dataPathFor;
|
|
41
|
+
exports.globToRegex = globToRegex;
|
|
42
|
+
exports.extractHostname = extractHostname;
|
|
43
|
+
exports.getDefaultKey = getDefaultKey;
|
|
44
|
+
exports.parsePermissionPattern = parsePermissionPattern;
|
|
45
|
+
exports.matchesPermission = matchesPermission;
|
|
46
|
+
exports.loadAllowPatterns = loadAllowPatterns;
|
|
47
|
+
exports.isSafeTool = isSafeTool;
|
|
48
|
+
exports.formatAskUserQuestion = formatAskUserQuestion;
|
|
49
|
+
exports.formatExitPlanMode = formatExitPlanMode;
|
|
50
|
+
exports.fmtK = fmtK;
|
|
51
|
+
exports.buildApprovalCard = buildApprovalCard;
|
|
52
|
+
exports.buildAskUserQuestionCard = buildAskUserQuestionCard;
|
|
53
|
+
exports.buildExitPlanModeCard = buildExitPlanModeCard;
|
|
54
|
+
exports.buildResolvedApprovalCard = buildResolvedApprovalCard;
|
|
55
|
+
exports.parseSnapshotCommand = parseSnapshotCommand;
|
|
56
|
+
exports.resolveKeySequence = resolveKeySequence;
|
|
57
|
+
exports.createFeishuHandler = createFeishuHandler;
|
|
58
|
+
exports.createWeixinHandler = createWeixinHandler;
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const os = __importStar(require("os"));
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
const base_1 = require("./feishu/cards/base");
|
|
63
|
+
// ============================================================
|
|
64
|
+
// Config loading
|
|
65
|
+
// ============================================================
|
|
66
|
+
function loadConfig() {
|
|
67
|
+
const userConfigPath = path.join(os.homedir(), '.cc-remote', 'config.json');
|
|
68
|
+
const localConfigPath = path.join(__dirname, '../config.json');
|
|
69
|
+
const configPath = fs.existsSync(userConfigPath) ? userConfigPath : localConfigPath;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error('\r\n[cc-remote] 读取 config.json 失败:', configPath, err);
|
|
75
|
+
console.error('[cc-remote] 请运行 cc-remote -config 配置飞书应用凭据');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ============================================================
|
|
80
|
+
// Data path helpers
|
|
81
|
+
// ============================================================
|
|
82
|
+
const dataRoot = path.join(os.homedir(), '.cc-remote', 'projects');
|
|
83
|
+
function getDataDir(targetDir) {
|
|
84
|
+
const slug = targetDir.replace(/^([A-Z]):/, '$1').replace(/[\/\\]/g, '-').toLowerCase();
|
|
85
|
+
const dir = path.join(dataRoot, slug);
|
|
86
|
+
if (!fs.existsSync(dir))
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
return dir;
|
|
89
|
+
}
|
|
90
|
+
function dataPath(targetDir, ...segments) {
|
|
91
|
+
const dir = getDataDir(targetDir);
|
|
92
|
+
return path.join(dir, ...segments);
|
|
93
|
+
}
|
|
94
|
+
function dataPathFor(projectDir, ...segments) {
|
|
95
|
+
const slug = projectDir.replace(/^([A-Z]):/, '$1').replace(/[\/\\]/g, '-').toLowerCase();
|
|
96
|
+
const dir = path.join(dataRoot, slug);
|
|
97
|
+
if (!fs.existsSync(dir))
|
|
98
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
99
|
+
return path.join(dir, ...segments);
|
|
100
|
+
}
|
|
101
|
+
// ============================================================
|
|
102
|
+
// Permission engine
|
|
103
|
+
// ============================================================
|
|
104
|
+
function globToRegex(pattern) {
|
|
105
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
106
|
+
return new RegExp('^' + escaped + '$');
|
|
107
|
+
}
|
|
108
|
+
function extractHostname(url) {
|
|
109
|
+
try {
|
|
110
|
+
const m = url.match(/^https?:\/\/([^/?#:]+)/);
|
|
111
|
+
return m ? m[1] : url;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return url;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function getDefaultKey(toolName) {
|
|
118
|
+
switch (toolName) {
|
|
119
|
+
case 'Bash': return 'command';
|
|
120
|
+
case 'Write':
|
|
121
|
+
case 'Edit': return 'file_path';
|
|
122
|
+
case 'WebFetch': return 'url';
|
|
123
|
+
case 'WebSearch': return 'query';
|
|
124
|
+
case 'Grep': return 'pattern';
|
|
125
|
+
case 'Glob': return 'pattern';
|
|
126
|
+
case 'Read': return 'file_path';
|
|
127
|
+
default: return '';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function parsePermissionPattern(pattern) {
|
|
131
|
+
const idx = pattern.indexOf('(');
|
|
132
|
+
if (idx === -1)
|
|
133
|
+
return { toolPattern: pattern };
|
|
134
|
+
const lastIdx = pattern.lastIndexOf(')');
|
|
135
|
+
if (lastIdx === -1 || lastIdx <= idx)
|
|
136
|
+
return { toolPattern: pattern };
|
|
137
|
+
return {
|
|
138
|
+
toolPattern: pattern.slice(0, idx),
|
|
139
|
+
condition: pattern.slice(idx + 1, lastIdx),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function matchesPermission(toolName, toolInput, pattern) {
|
|
143
|
+
const { toolPattern, condition } = parsePermissionPattern(pattern);
|
|
144
|
+
if (!globToRegex(toolPattern).test(toolName))
|
|
145
|
+
return false;
|
|
146
|
+
if (!condition || condition === '*')
|
|
147
|
+
return true;
|
|
148
|
+
let key;
|
|
149
|
+
let valueGlob;
|
|
150
|
+
const colonIdx = condition.indexOf(':');
|
|
151
|
+
if (colonIdx !== -1) {
|
|
152
|
+
key = condition.slice(0, colonIdx).trim();
|
|
153
|
+
valueGlob = condition.slice(colonIdx + 1).trim();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
key = getDefaultKey(toolName);
|
|
157
|
+
valueGlob = condition;
|
|
158
|
+
}
|
|
159
|
+
if (!key)
|
|
160
|
+
return false;
|
|
161
|
+
if (key === 'domain') {
|
|
162
|
+
const url = String(toolInput['url'] ?? '');
|
|
163
|
+
return globToRegex(valueGlob).test(extractHostname(url));
|
|
164
|
+
}
|
|
165
|
+
return globToRegex(valueGlob).test(String(toolInput[key] ?? ''));
|
|
166
|
+
}
|
|
167
|
+
function loadAllowPatterns(projectDir) {
|
|
168
|
+
const patterns = [];
|
|
169
|
+
const settingsPaths = [
|
|
170
|
+
path.join(projectDir, '.claude', 'settings.json'),
|
|
171
|
+
path.join(projectDir, '.claude', 'settings.local.json'),
|
|
172
|
+
];
|
|
173
|
+
for (const p of settingsPaths) {
|
|
174
|
+
if (!fs.existsSync(p))
|
|
175
|
+
continue;
|
|
176
|
+
try {
|
|
177
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
178
|
+
const cfg = JSON.parse(raw);
|
|
179
|
+
const allow = cfg?.permissions?.allow;
|
|
180
|
+
if (Array.isArray(allow)) {
|
|
181
|
+
for (const item of allow) {
|
|
182
|
+
if (typeof item === 'string' && !patterns.includes(item)) {
|
|
183
|
+
patterns.push(item);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch { /* skip */ }
|
|
189
|
+
}
|
|
190
|
+
return patterns;
|
|
191
|
+
}
|
|
192
|
+
exports.safeTools = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'AskUserQuestion', 'Skill', 'TaskCreate', 'ExitPlanMode']);
|
|
193
|
+
function isSafeTool(toolName) {
|
|
194
|
+
return exports.safeTools.has(toolName);
|
|
195
|
+
}
|
|
196
|
+
// ============================================================
|
|
197
|
+
// Text formatters
|
|
198
|
+
// ============================================================
|
|
199
|
+
function formatAskUserQuestion(input) {
|
|
200
|
+
const questions = Array.isArray(input.questions) ? input.questions : [];
|
|
201
|
+
const parts = [];
|
|
202
|
+
let num = 1;
|
|
203
|
+
for (let qi = 0; qi < questions.length; qi++) {
|
|
204
|
+
const q = questions[qi];
|
|
205
|
+
if (questions.length > 1)
|
|
206
|
+
parts.push(`── 问题 ${qi + 1}/${questions.length} ──`);
|
|
207
|
+
const header = typeof q.header === 'string' ? q.header : '';
|
|
208
|
+
const question = typeof q.question === 'string' ? q.question : '';
|
|
209
|
+
parts.push(`💬 **${header}**`);
|
|
210
|
+
parts.push(question);
|
|
211
|
+
parts.push('');
|
|
212
|
+
const options = Array.isArray(q.options) ? q.options : [];
|
|
213
|
+
for (let i = 0; i < options.length; i++) {
|
|
214
|
+
const label = typeof options[i].label === 'string' ? options[i].label : '';
|
|
215
|
+
const desc = typeof options[i].description === 'string' ? options[i].description : '';
|
|
216
|
+
parts.push(`${num++}. **${label}**`);
|
|
217
|
+
if (desc)
|
|
218
|
+
parts.push(` ${desc}`);
|
|
219
|
+
}
|
|
220
|
+
parts.push('');
|
|
221
|
+
}
|
|
222
|
+
parts.push(`${num++}. **Type something.**`);
|
|
223
|
+
parts.push(`${num++}. **Chat about this**`);
|
|
224
|
+
parts.push(`${num++}. **Skip interview and plan immediately**`);
|
|
225
|
+
parts.push('');
|
|
226
|
+
parts.push('回复/esc取消');
|
|
227
|
+
return parts.join('\n');
|
|
228
|
+
}
|
|
229
|
+
function formatExitPlanMode(input) {
|
|
230
|
+
const plan = typeof input.plan === 'string' ? input.plan : '';
|
|
231
|
+
const lines = plan.split('\n');
|
|
232
|
+
const title = lines[0].replace(/^#+\s*/, '');
|
|
233
|
+
const bodyLines = lines.slice(1).join('\n').trim();
|
|
234
|
+
return `📋 计划: ${title || 'ExitPlanMode'}\n\n${bodyLines.length > 5000 ? bodyLines.slice(0, 5000) + '\n…' : bodyLines}\n\n1. Yes, auto-accept edits\n2. Yes, manually approve edits\n3. Tell Claude what to change`;
|
|
235
|
+
}
|
|
236
|
+
function fmtK(n) {
|
|
237
|
+
if (n === undefined || n === 0)
|
|
238
|
+
return '0';
|
|
239
|
+
if (n >= 1000)
|
|
240
|
+
return (n / 1000).toFixed(1) + 'K';
|
|
241
|
+
return String(n);
|
|
242
|
+
}
|
|
243
|
+
// ============================================================
|
|
244
|
+
// Card builders
|
|
245
|
+
// ============================================================
|
|
246
|
+
function buildApprovalCard(toolName, toolInput, requestId) {
|
|
247
|
+
return {
|
|
248
|
+
schema: '2.0',
|
|
249
|
+
config: { wide_screen_mode: true },
|
|
250
|
+
header: {
|
|
251
|
+
title: { tag: 'plain_text', content: '🔐 工具审批' },
|
|
252
|
+
template: 'orange',
|
|
253
|
+
},
|
|
254
|
+
body: {
|
|
255
|
+
elements: [
|
|
256
|
+
{ tag: 'markdown', content: `请求调用:**${toolName}**` },
|
|
257
|
+
{ tag: 'markdown', content: `\`\`\`\n${toolInput}\n\`\`\`` },
|
|
258
|
+
{ tag: 'hr' },
|
|
259
|
+
(0, base_1.btnRow)([
|
|
260
|
+
{ label: '❌ 拒绝', style: 'danger', value: { cmd: '__approve', requestId, toolName, decision: 'deny' } },
|
|
261
|
+
{ label: '🟢 始终允许', style: 'default', value: { cmd: '__approve', requestId, toolName, decision: 'always_allow' } },
|
|
262
|
+
{ label: '✅ 允许', style: 'primary', value: { cmd: '__approve', requestId, toolName, decision: 'allow' } },
|
|
263
|
+
]),
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function buildAskUserQuestionCard(toolInput) {
|
|
269
|
+
const fullText = formatAskUserQuestion(toolInput);
|
|
270
|
+
const bodyText = fullText.replace(/\n?回复\/esc取消$/, '');
|
|
271
|
+
return {
|
|
272
|
+
schema: '2.0',
|
|
273
|
+
config: { wide_screen_mode: true },
|
|
274
|
+
header: {
|
|
275
|
+
title: { tag: 'plain_text', content: '💬 Claude 询问' },
|
|
276
|
+
template: 'blue',
|
|
277
|
+
},
|
|
278
|
+
body: {
|
|
279
|
+
elements: [
|
|
280
|
+
{ tag: 'markdown', content: bodyText },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function buildExitPlanModeCard(toolInput) {
|
|
286
|
+
const plan = typeof toolInput.plan === 'string' ? toolInput.plan : '';
|
|
287
|
+
return {
|
|
288
|
+
schema: '2.0',
|
|
289
|
+
config: { wide_screen_mode: true },
|
|
290
|
+
header: {
|
|
291
|
+
title: { tag: 'plain_text', content: '📋 计划' },
|
|
292
|
+
template: 'blue',
|
|
293
|
+
},
|
|
294
|
+
body: {
|
|
295
|
+
elements: [
|
|
296
|
+
{ tag: 'markdown', content: `**ExitPlanMode**` },
|
|
297
|
+
{ tag: 'markdown', content: plan },
|
|
298
|
+
{ tag: 'hr' },
|
|
299
|
+
{ tag: 'markdown', content: '1. Yes, auto-accept edits\n2. Yes, manually approve edits\n3. Tell Claude what to change' },
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function buildResolvedApprovalCard(toolName, decision) {
|
|
305
|
+
const isAllowed = decision === 'allow' || decision === 'always_allow';
|
|
306
|
+
const label = decision === 'always_allow' ? '🟢 始终允许' : isAllowed ? '✅ 已允许' : '❌ 已拒绝';
|
|
307
|
+
return {
|
|
308
|
+
schema: '2.0',
|
|
309
|
+
config: { wide_screen_mode: true },
|
|
310
|
+
header: {
|
|
311
|
+
title: { tag: 'plain_text', content: label },
|
|
312
|
+
template: (isAllowed ? 'green' : 'red'),
|
|
313
|
+
},
|
|
314
|
+
body: {
|
|
315
|
+
elements: [
|
|
316
|
+
{ tag: 'markdown', content: `工具:**${toolName}**` },
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function parseSnapshotCommand(text) {
|
|
322
|
+
const m = text.trim().toLowerCase().match(/^\/(?:snapshot|快照)(\d*)$/);
|
|
323
|
+
if (!m)
|
|
324
|
+
return null;
|
|
325
|
+
if (m[1])
|
|
326
|
+
return { type: 'last', count: parseInt(m[1], 10) || 10 };
|
|
327
|
+
return { type: 'separator' };
|
|
328
|
+
}
|
|
329
|
+
const KEY_SEQ_MAP = {
|
|
330
|
+
'/up': '\x1b[A', '/上': '\x1b[A',
|
|
331
|
+
'/down': '\x1b[B', '/下': '\x1b[B',
|
|
332
|
+
'/left': '\x1b[D', '/左': '\x1b[D',
|
|
333
|
+
'/right': '\x1b[C', '/右': '\x1b[C',
|
|
334
|
+
'/enter': '\r', '/回车': '\r',
|
|
335
|
+
'/esc': '\x1b', '/取消': '\x1b',
|
|
336
|
+
'/切换': '\x1b[Z',
|
|
337
|
+
'/space': ' ', '/空格': ' ',
|
|
338
|
+
};
|
|
339
|
+
function resolveKeySequence(text) {
|
|
340
|
+
return KEY_SEQ_MAP[text.trim().toLowerCase()] ?? null;
|
|
341
|
+
}
|
|
342
|
+
function createFeishuHandler(deps) {
|
|
343
|
+
const { getPty, feishuReplier, onStatus, dataFile, openId, onSnapshot, onMessageReceived, messageLog } = deps;
|
|
344
|
+
async function handleSnapshot(mode, chatId) {
|
|
345
|
+
if (onSnapshot) {
|
|
346
|
+
await onSnapshot(mode, 'feishu', { chatId });
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
onStatus('\r\n[快照] 终端快照在 CLI 模式下不可用\r\n');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return (text, meta) => {
|
|
353
|
+
messageLog(`[飞书←] chatId=${meta.chatId}: ${text}`);
|
|
354
|
+
const snap = parseSnapshotCommand(text);
|
|
355
|
+
if (snap) {
|
|
356
|
+
handleSnapshot(snap, meta.chatId).catch(() => { });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const keySeq = resolveKeySequence(text);
|
|
360
|
+
const p = getPty();
|
|
361
|
+
if (keySeq && p) {
|
|
362
|
+
p.write(keySeq);
|
|
363
|
+
onStatus(`\r\n[飞书] 快捷键: ${text}\r\n`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
onStatus(`\r\n[飞书] 收到: ${text}\r\n`);
|
|
367
|
+
onMessageReceived?.(text);
|
|
368
|
+
const contextPath = dataFile('cc_remote_context.json');
|
|
369
|
+
let existingMode = 'close';
|
|
370
|
+
if (fs.existsSync(contextPath)) {
|
|
371
|
+
try {
|
|
372
|
+
existingMode = JSON.parse(fs.readFileSync(contextPath, 'utf-8')).mode || 'close';
|
|
373
|
+
}
|
|
374
|
+
catch { /* ignore */ }
|
|
375
|
+
}
|
|
376
|
+
fs.writeFileSync(contextPath, JSON.stringify({
|
|
377
|
+
chatId: meta.chatId,
|
|
378
|
+
openId,
|
|
379
|
+
messageId: meta.messageId,
|
|
380
|
+
mode: existingMode,
|
|
381
|
+
channel: 'feishu',
|
|
382
|
+
}));
|
|
383
|
+
if (p) {
|
|
384
|
+
p.write(text + '\r');
|
|
385
|
+
}
|
|
386
|
+
feishuReplier.createReaction(meta.messageId, 'Typing').catch(() => { });
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function createWeixinHandler(deps) {
|
|
390
|
+
const { getPty, weixinReplier, onStatus, dataFile, wxPendingApprovals, onSnapshot, onMessageReceived, messageLog } = deps;
|
|
391
|
+
async function handleSnapshot(mode, meta) {
|
|
392
|
+
if (onSnapshot) {
|
|
393
|
+
await onSnapshot(mode, 'weixin', meta);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
onStatus('\r\n[快照] 终端快照在 CLI 模式下不可用\r\n');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return (text, meta) => {
|
|
400
|
+
messageLog(`[微信←] userId=${meta.userId}: ${text}`);
|
|
401
|
+
const snap = parseSnapshotCommand(text);
|
|
402
|
+
if (snap) {
|
|
403
|
+
handleSnapshot(snap, { userId: meta.userId, contextToken: meta.contextToken }).catch(() => { });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const keySeq = resolveKeySequence(text);
|
|
407
|
+
const p = getPty();
|
|
408
|
+
if (keySeq && p) {
|
|
409
|
+
p.write(keySeq);
|
|
410
|
+
onStatus(`\r\n[微信] 快捷键: ${text}\r\n`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
onStatus(`\r\n[微信] 收到: ${text}\r\n`);
|
|
414
|
+
onMessageReceived?.(text);
|
|
415
|
+
// Check for pending approval response
|
|
416
|
+
const trimmed = text.trim().toLowerCase();
|
|
417
|
+
const wxPending = wxPendingApprovals.get(meta.userId);
|
|
418
|
+
if (wxPending) {
|
|
419
|
+
if (trimmed === 'go' || trimmed === 'no' || trimmed === 'ok') {
|
|
420
|
+
clearTimeout(wxPending.timer);
|
|
421
|
+
wxPendingApprovals.delete(meta.userId);
|
|
422
|
+
const decision = trimmed === 'no' ? 'deny'
|
|
423
|
+
: trimmed === 'ok' ? 'always_allow'
|
|
424
|
+
: 'allow';
|
|
425
|
+
wxPending.resolve(decision);
|
|
426
|
+
const confirmText = trimmed === 'no' ? '❌ 已拒绝'
|
|
427
|
+
: trimmed === 'ok' ? '🟢 已始终允许并记住'
|
|
428
|
+
: '✅ 已允许';
|
|
429
|
+
weixinReplier.sendText(meta.userId, confirmText, meta.contextToken);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const contextPath = dataFile('cc_remote_context.json');
|
|
434
|
+
let existingMode = 'close';
|
|
435
|
+
if (fs.existsSync(contextPath)) {
|
|
436
|
+
try {
|
|
437
|
+
existingMode = JSON.parse(fs.readFileSync(contextPath, 'utf-8')).mode || 'close';
|
|
438
|
+
}
|
|
439
|
+
catch { /* ignore */ }
|
|
440
|
+
}
|
|
441
|
+
fs.writeFileSync(contextPath, JSON.stringify({
|
|
442
|
+
userId: meta.userId,
|
|
443
|
+
contextToken: meta.contextToken,
|
|
444
|
+
messageId: meta.messageId,
|
|
445
|
+
mode: existingMode,
|
|
446
|
+
channel: 'weixin',
|
|
447
|
+
}));
|
|
448
|
+
if (p) {
|
|
449
|
+
p.write(text + '\r');
|
|
450
|
+
}
|
|
451
|
+
weixinReplier.sendText(meta.userId, '☕', meta.contextToken, meta.messageId).catch(() => { });
|
|
452
|
+
};
|
|
453
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.translateSdkMessage = translateSdkMessage;
|
|
4
|
+
function translateSdkMessage(msg) {
|
|
5
|
+
const out = [];
|
|
6
|
+
if (msg.type === 'system') {
|
|
7
|
+
if (msg.subtype === 'init') {
|
|
8
|
+
out.push({ kind: 'init', sessionId: msg.session_id ?? '' });
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
if (msg.type === 'assistant') {
|
|
13
|
+
const blocks = msg.message?.content ?? [];
|
|
14
|
+
for (const b of blocks) {
|
|
15
|
+
if (b['type'] === 'text' && typeof b['text'] === 'string') {
|
|
16
|
+
out.push({ kind: 'assistant-text', text: b['text'] });
|
|
17
|
+
}
|
|
18
|
+
else if (b['type'] === 'tool_use') {
|
|
19
|
+
out.push({
|
|
20
|
+
kind: 'tool-use',
|
|
21
|
+
id: b['id'] ?? '',
|
|
22
|
+
name: b['name'] ?? '',
|
|
23
|
+
input: b['input'],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
if (msg.type === 'user') {
|
|
30
|
+
const rawContent = msg.message?.content ?? [];
|
|
31
|
+
if (Array.isArray(rawContent)) {
|
|
32
|
+
for (const b of rawContent) {
|
|
33
|
+
if (b['type'] === 'tool_result') {
|
|
34
|
+
out.push({
|
|
35
|
+
kind: 'tool-result',
|
|
36
|
+
toolUseId: b['tool_use_id'] ?? '',
|
|
37
|
+
content: stringifyContent(b['content']),
|
|
38
|
+
isError: Boolean(b['is_error']),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
if (msg.type === 'result') {
|
|
46
|
+
const errors = Array.isArray(msg.errors) && msg.errors.length > 0 ? msg.errors : undefined;
|
|
47
|
+
const errorDetail = errors ? errors.join('\n') : '';
|
|
48
|
+
const text = msg.result || errorDetail || '';
|
|
49
|
+
out.push({
|
|
50
|
+
kind: 'result',
|
|
51
|
+
ok: !msg.is_error,
|
|
52
|
+
text,
|
|
53
|
+
durationMs: msg.duration_ms ?? 0,
|
|
54
|
+
usage: msg.usage ? {
|
|
55
|
+
inputTokens: msg.usage.input_tokens ?? 0,
|
|
56
|
+
outputTokens: msg.usage.output_tokens ?? 0,
|
|
57
|
+
cacheReadTokens: msg.usage.cache_read_input_tokens ?? 0,
|
|
58
|
+
cacheCreationTokens: msg.usage.cache_creation_input_tokens ?? 0,
|
|
59
|
+
} : undefined,
|
|
60
|
+
errors,
|
|
61
|
+
});
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function stringifyContent(c) {
|
|
67
|
+
if (typeof c === 'string')
|
|
68
|
+
return c;
|
|
69
|
+
if (!Array.isArray(c))
|
|
70
|
+
return '';
|
|
71
|
+
return c
|
|
72
|
+
.map((b) => {
|
|
73
|
+
if (b['type'] === 'text' && typeof b['text'] === 'string')
|
|
74
|
+
return b['text'];
|
|
75
|
+
return JSON.stringify(b);
|
|
76
|
+
})
|
|
77
|
+
.join('\n');
|
|
78
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.plainText = plainText;
|
|
4
|
+
exports.md = md;
|
|
5
|
+
exports.hr = hr;
|
|
6
|
+
exports.btnRow = btnRow;
|
|
7
|
+
exports.cmdBtn = cmdBtn;
|
|
8
|
+
exports.cmdBtnRefresh = cmdBtnRefresh;
|
|
9
|
+
exports.toastBtn = toastBtn;
|
|
10
|
+
exports.cardHeader = cardHeader;
|
|
11
|
+
exports.card = card;
|
|
12
|
+
exports.textCard = textCard;
|
|
13
|
+
exports.truncate = truncate;
|
|
14
|
+
exports.limitTables = limitTables;
|
|
15
|
+
exports.projectName = projectName;
|
|
16
|
+
function plainText(text) {
|
|
17
|
+
return { tag: 'plain_text', content: text };
|
|
18
|
+
}
|
|
19
|
+
function md(content) {
|
|
20
|
+
return { tag: 'markdown', content };
|
|
21
|
+
}
|
|
22
|
+
function hr() {
|
|
23
|
+
return { tag: 'hr' };
|
|
24
|
+
}
|
|
25
|
+
function btn(spec) {
|
|
26
|
+
return {
|
|
27
|
+
tag: 'button',
|
|
28
|
+
text: plainText(spec.label),
|
|
29
|
+
type: spec.style ?? 'default',
|
|
30
|
+
size: 'medium',
|
|
31
|
+
width: 'default',
|
|
32
|
+
behaviors: [{ type: 'callback', value: spec.value }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function btnRow(buttons) {
|
|
36
|
+
return {
|
|
37
|
+
tag: 'column_set',
|
|
38
|
+
flex_mode: 'wrap',
|
|
39
|
+
horizontal_spacing: '8px',
|
|
40
|
+
columns: buttons.map((b) => ({
|
|
41
|
+
tag: 'column',
|
|
42
|
+
width: 'weighted',
|
|
43
|
+
weight: 1,
|
|
44
|
+
vertical_align: 'center',
|
|
45
|
+
elements: [btn(b)],
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function cmdBtn(label, cmd, args, style) {
|
|
50
|
+
return { label, style, value: { cmd, args } };
|
|
51
|
+
}
|
|
52
|
+
function cmdBtnRefresh(label, cmd, args, refresh, style) {
|
|
53
|
+
return { label, style, value: { cmd, args, refresh } };
|
|
54
|
+
}
|
|
55
|
+
function toastBtn(label, toast, style) {
|
|
56
|
+
return { label, style, value: { echo: toast, silent: true } };
|
|
57
|
+
}
|
|
58
|
+
function cardHeader(title, color = 'blue') {
|
|
59
|
+
return {
|
|
60
|
+
title: plainText(title),
|
|
61
|
+
template: color,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function card(h, elements) {
|
|
65
|
+
return {
|
|
66
|
+
schema: '2.0',
|
|
67
|
+
config: { wide_screen_mode: true },
|
|
68
|
+
header: h,
|
|
69
|
+
body: { elements },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function textCard(text, title = '📬 回复', color = 'blue') {
|
|
73
|
+
return card(cardHeader(title, color), [md(text)]);
|
|
74
|
+
}
|
|
75
|
+
function truncate(s, max) {
|
|
76
|
+
return s.length <= max ? s : s.slice(0, max) + '\n\n… (已截断)';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 飞书卡片有表格数量上限(ErrCode 11310: card table number over limit)。
|
|
80
|
+
* 当 Markdown 中表格过多时,将多余的表格转为纯文本列表以避免 400 错误。
|
|
81
|
+
*/
|
|
82
|
+
function limitTables(s, max = 3) {
|
|
83
|
+
const sepRegex = /^\|[-:\s|]+\|$/gm;
|
|
84
|
+
const matches = s.match(sepRegex);
|
|
85
|
+
if (!matches || matches.length <= max)
|
|
86
|
+
return s;
|
|
87
|
+
let tableIdx = 0;
|
|
88
|
+
let inTable = false;
|
|
89
|
+
return s
|
|
90
|
+
.split('\n')
|
|
91
|
+
.map((line) => {
|
|
92
|
+
const isTableLine = /^\s*\|/.test(line);
|
|
93
|
+
const isSep = isTableLine && /^[\s|:-]+$/.test(line);
|
|
94
|
+
if (isSep && !inTable) {
|
|
95
|
+
inTable = true;
|
|
96
|
+
tableIdx++;
|
|
97
|
+
}
|
|
98
|
+
if (!isTableLine)
|
|
99
|
+
inTable = false;
|
|
100
|
+
if (!isTableLine || tableIdx <= max)
|
|
101
|
+
return line;
|
|
102
|
+
if (isSep)
|
|
103
|
+
return '';
|
|
104
|
+
return line
|
|
105
|
+
.replace(/^\|\s*/, ' ')
|
|
106
|
+
.replace(/\s*\|\s*$/, '')
|
|
107
|
+
.replace(/\s*\|\s*/g, ' | ');
|
|
108
|
+
})
|
|
109
|
+
.filter((l) => l !== '')
|
|
110
|
+
.join('\n');
|
|
111
|
+
}
|
|
112
|
+
function projectName(cwd) {
|
|
113
|
+
return cwd.split('/').filter(Boolean).pop() ?? '';
|
|
114
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderHelpCard = renderHelpCard;
|
|
4
|
+
const base_1 = require("./base");
|
|
5
|
+
function renderHelpCard() {
|
|
6
|
+
return (0, base_1.card)((0, base_1.cardHeader)('📋 命令手册', 'blue'), [
|
|
7
|
+
(0, base_1.md)('**🎯 常用功能**'),
|
|
8
|
+
(0, base_1.btnRow)([
|
|
9
|
+
(0, base_1.cmdBtn)('📊 状态', 'status', ''),
|
|
10
|
+
(0, base_1.cmdBtn)('📂 项目', 'project', ''),
|
|
11
|
+
]),
|
|
12
|
+
(0, base_1.btnRow)([
|
|
13
|
+
(0, base_1.cmdBtn)('📋 会话', 'session', 'list'),
|
|
14
|
+
(0, base_1.cmdBtn)('💰 用量', 'usage', ''),
|
|
15
|
+
]),
|
|
16
|
+
(0, base_1.hr)(),
|
|
17
|
+
(0, base_1.md)('**💬 会话交互**\n' +
|
|
18
|
+
'`/s <消息>` 发送到当前活跃会话\n' +
|
|
19
|
+
'`/session start` 开启新会话\n' +
|
|
20
|
+
'`/session list` 列出所有会话\n' +
|
|
21
|
+
'`/session stop [slot名]` 关闭会话\n' +
|
|
22
|
+
'`/stop` 中断当前任务'),
|
|
23
|
+
(0, base_1.hr)(),
|
|
24
|
+
(0, base_1.md)('**🤖 无状态问答**\n' +
|
|
25
|
+
'`/ask <提示词>` 一次性问答'),
|
|
26
|
+
(0, base_1.hr)(),
|
|
27
|
+
(0, base_1.md)('**🛠 管理**\n' +
|
|
28
|
+
'`/status` 系统状态 · `/help` 帮助\n' +
|
|
29
|
+
'`/usage` Token 用量 · `/ping` 健康检查'),
|
|
30
|
+
(0, base_1.hr)(),
|
|
31
|
+
(0, base_1.md)('*💬 直接发消息到活跃会话 · /help 查看详情*'),
|
|
32
|
+
]);
|
|
33
|
+
}
|