@neomei/opencode-feishu 0.1.0
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/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/bin/opencode-feishu +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +273 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/config.d.ts +48 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +76 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/daemon.d.ts +42 -0
- package/dist/core/daemon.d.ts.map +1 -0
- package/dist/core/daemon.js +134 -0
- package/dist/core/daemon.js.map +1 -0
- package/dist/core/logger.d.ts +10 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +42 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/message-handler.d.ts +13 -0
- package/dist/core/message-handler.d.ts.map +1 -0
- package/dist/core/message-handler.js +120 -0
- package/dist/core/message-handler.js.map +1 -0
- package/dist/core/session-manager.d.ts +34 -0
- package/dist/core/session-manager.d.ts.map +1 -0
- package/dist/core/session-manager.js +187 -0
- package/dist/core/session-manager.js.map +1 -0
- package/dist/core/types.d.ts +87 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/feishu/api.d.ts +23 -0
- package/dist/feishu/api.d.ts.map +1 -0
- package/dist/feishu/api.js +97 -0
- package/dist/feishu/api.js.map +1 -0
- package/dist/feishu/card.d.ts +11 -0
- package/dist/feishu/card.d.ts.map +1 -0
- package/dist/feishu/card.js +62 -0
- package/dist/feishu/card.js.map +1 -0
- package/dist/feishu/event-source.d.ts +19 -0
- package/dist/feishu/event-source.d.ts.map +1 -0
- package/dist/feishu/event-source.js +70 -0
- package/dist/feishu/event-source.js.map +1 -0
- package/dist/feishu/silent-logger.d.ts +16 -0
- package/dist/feishu/silent-logger.d.ts.map +1 -0
- package/dist/feishu/silent-logger.js +16 -0
- package/dist/feishu/silent-logger.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/opencode/client.d.ts +27 -0
- package/dist/opencode/client.d.ts.map +1 -0
- package/dist/opencode/client.js +86 -0
- package/dist/opencode/client.js.map +1 -0
- package/dist/opencode/event-handler.d.ts +26 -0
- package/dist/opencode/event-handler.d.ts.map +1 -0
- package/dist/opencode/event-handler.js +212 -0
- package/dist/opencode/event-handler.js.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +64 -0
- package/dist/plugin.js.map +1 -0
- package/dist/setup/preflight.d.ts +29 -0
- package/dist/setup/preflight.d.ts.map +1 -0
- package/dist/setup/preflight.js +125 -0
- package/dist/setup/preflight.js.map +1 -0
- package/dist/setup/wizard.d.ts +11 -0
- package/dist/setup/wizard.d.ts.map +1 -0
- package/dist/setup/wizard.js +263 -0
- package/dist/setup/wizard.js.map +1 -0
- package/dist/standalone.d.ts +2 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +116 -0
- package/dist/standalone.js.map +1 -0
- package/dist/types/plugin.d.ts +23 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +2 -0
- package/dist/types/plugin.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const PID_FILE: string;
|
|
2
|
+
export declare const STATUS_FILE: string;
|
|
3
|
+
export interface StatusSnapshot {
|
|
4
|
+
pid: number;
|
|
5
|
+
startedAt: number;
|
|
6
|
+
lastHeartbeat: number;
|
|
7
|
+
sessionCount: number;
|
|
8
|
+
feishuConnected: boolean;
|
|
9
|
+
opencodeUrl: string;
|
|
10
|
+
version: 1;
|
|
11
|
+
}
|
|
12
|
+
export interface HeartbeatSource {
|
|
13
|
+
getSessionCount(): number;
|
|
14
|
+
isFeishuConnected(): boolean;
|
|
15
|
+
getOpencodeUrl(): string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fork current process detached, redirect stdout/stderr to the log file,
|
|
19
|
+
* write child PID to PID file, and exit parent.
|
|
20
|
+
*
|
|
21
|
+
* The child inherits FEISHU_DAEMONIZED=1 and is invoked with the original
|
|
22
|
+
* `start` args minus `--daemon`, so it runs a normal foreground start.
|
|
23
|
+
*/
|
|
24
|
+
export declare function spawnDaemon(startArgs: string[]): void;
|
|
25
|
+
/**
|
|
26
|
+
* Start periodic status-file writer. Returns a stop() function for shutdown.
|
|
27
|
+
*/
|
|
28
|
+
export declare function startStatusWriter(source: HeartbeatSource): () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Read the most recent status snapshot. Returns null if the plugin isn't
|
|
31
|
+
* running or the file is missing / corrupt / stale.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readStatus(): StatusSnapshot | null;
|
|
34
|
+
export declare function readPid(): number | null;
|
|
35
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
36
|
+
export declare function statusFileAgeMs(): number | null;
|
|
37
|
+
/**
|
|
38
|
+
* Heartbeat freshness threshold. If the status file is older than this,
|
|
39
|
+
* the daemon is presumed stuck (even if the PID is still alive).
|
|
40
|
+
*/
|
|
41
|
+
export declare const HEARTBEAT_STALE_AFTER_MS: number;
|
|
42
|
+
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/core/daemon.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,QAAQ,QAAuD,CAAC;AAC7E,eAAO,MAAM,WAAW,QAA+D,CAAC;AAKxF,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,IAAI,MAAM,CAAC;IAC1B,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,MAAM,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CA+BrD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,IAAI,CA6BrE;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,cAAc,GAAG,IAAI,CAUlD;AAED,wBAAgB,OAAO,IAAI,MAAM,GAAG,IAAI,CASvC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAO/C;AAED;;;GAGG;AACH,eAAO,MAAM,wBAAwB,QAA4B,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync, openSync, writeFileSync, readFileSync, unlinkSync, statSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createLogger } from './logger.js';
|
|
7
|
+
const log = createLogger('daemon');
|
|
8
|
+
export const PID_FILE = join(homedir(), '.config', 'opencode', 'feishu.pid');
|
|
9
|
+
export const STATUS_FILE = join(homedir(), '.config', 'opencode', 'feishu-status.json');
|
|
10
|
+
const DEFAULT_LOG_FILE = join(homedir(), '.config', 'opencode', 'feishu.log');
|
|
11
|
+
const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
12
|
+
/**
|
|
13
|
+
* Fork current process detached, redirect stdout/stderr to the log file,
|
|
14
|
+
* write child PID to PID file, and exit parent.
|
|
15
|
+
*
|
|
16
|
+
* The child inherits FEISHU_DAEMONIZED=1 and is invoked with the original
|
|
17
|
+
* `start` args minus `--daemon`, so it runs a normal foreground start.
|
|
18
|
+
*/
|
|
19
|
+
export function spawnDaemon(startArgs) {
|
|
20
|
+
const logFile = process.env.FEISHU_LOG_FILE || DEFAULT_LOG_FILE;
|
|
21
|
+
const logDir = dirname(logFile);
|
|
22
|
+
if (!existsSync(logDir))
|
|
23
|
+
mkdirSync(logDir, { recursive: true });
|
|
24
|
+
// Ensure log file exists before opening fd
|
|
25
|
+
if (!existsSync(logFile))
|
|
26
|
+
writeFileSync(logFile, '');
|
|
27
|
+
const logFd = openSync(logFile, 'a');
|
|
28
|
+
// Resolve the script we're running: bin/opencode-feishu → dist/cli.js
|
|
29
|
+
// When invoked via the bin wrapper, argv[1] is the cli.js path already.
|
|
30
|
+
const scriptPath = fileURLToPath(import.meta.url).replace(/\/core\/daemon\.js$/, '/cli.js');
|
|
31
|
+
const child = spawn(process.execPath, [scriptPath, 'start', ...startArgs.filter(a => a !== '--daemon' && a !== '-d')], {
|
|
32
|
+
detached: true,
|
|
33
|
+
stdio: ['ignore', logFd, logFd],
|
|
34
|
+
env: { ...process.env, FEISHU_DAEMONIZED: '1' },
|
|
35
|
+
});
|
|
36
|
+
const pidDir = dirname(PID_FILE);
|
|
37
|
+
if (!existsSync(pidDir))
|
|
38
|
+
mkdirSync(pidDir, { recursive: true });
|
|
39
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
40
|
+
child.unref();
|
|
41
|
+
console.log(`✅ Plugin daemonized (PID: ${child.pid})`);
|
|
42
|
+
console.log(` Logs: ${logFile}`);
|
|
43
|
+
console.log(` Status: opencode-feishu status`);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Start periodic status-file writer. Returns a stop() function for shutdown.
|
|
47
|
+
*/
|
|
48
|
+
export function startStatusWriter(source) {
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
const write = () => {
|
|
51
|
+
const snap = {
|
|
52
|
+
pid: process.pid,
|
|
53
|
+
startedAt,
|
|
54
|
+
lastHeartbeat: Date.now(),
|
|
55
|
+
sessionCount: source.getSessionCount(),
|
|
56
|
+
feishuConnected: source.isFeishuConnected(),
|
|
57
|
+
opencodeUrl: source.getOpencodeUrl(),
|
|
58
|
+
version: 1,
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
const dir = dirname(STATUS_FILE);
|
|
62
|
+
if (!existsSync(dir))
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
writeFileSync(STATUS_FILE, JSON.stringify(snap, null, 2));
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
log.warn({ err }, 'Failed to write status file');
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
write();
|
|
71
|
+
const timer = setInterval(write, HEARTBEAT_INTERVAL_MS);
|
|
72
|
+
return () => {
|
|
73
|
+
clearInterval(timer);
|
|
74
|
+
try {
|
|
75
|
+
unlinkSync(STATUS_FILE);
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Read the most recent status snapshot. Returns null if the plugin isn't
|
|
82
|
+
* running or the file is missing / corrupt / stale.
|
|
83
|
+
*/
|
|
84
|
+
export function readStatus() {
|
|
85
|
+
if (!existsSync(STATUS_FILE))
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
const raw = readFileSync(STATUS_FILE, 'utf-8');
|
|
89
|
+
const snap = JSON.parse(raw);
|
|
90
|
+
if (snap.version !== 1)
|
|
91
|
+
return null;
|
|
92
|
+
return snap;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function readPid() {
|
|
99
|
+
if (!existsSync(PID_FILE))
|
|
100
|
+
return null;
|
|
101
|
+
try {
|
|
102
|
+
const raw = readFileSync(PID_FILE, 'utf-8').trim();
|
|
103
|
+
const pid = parseInt(raw, 10);
|
|
104
|
+
return Number.isFinite(pid) ? pid : null;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function isProcessAlive(pid) {
|
|
111
|
+
try {
|
|
112
|
+
process.kill(pid, 0);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function statusFileAgeMs() {
|
|
120
|
+
if (!existsSync(STATUS_FILE))
|
|
121
|
+
return null;
|
|
122
|
+
try {
|
|
123
|
+
return Date.now() - statSync(STATUS_FILE).mtimeMs;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Heartbeat freshness threshold. If the status file is older than this,
|
|
131
|
+
* the daemon is presumed stuck (even if the PID is still alive).
|
|
132
|
+
*/
|
|
133
|
+
export const HEARTBEAT_STALE_AFTER_MS = HEARTBEAT_INTERVAL_MS * 3;
|
|
134
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon.js","sourceRoot":"","sources":["../../src/core/daemon.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Logger } from 'pino';
|
|
2
|
+
export declare const rootLogger: Logger;
|
|
3
|
+
/**
|
|
4
|
+
* Create a child logger with a module tag. Use this in every src/ module:
|
|
5
|
+
* const log = createLogger('FeishuAPI');
|
|
6
|
+
* log.info({ chatId }, 'sending card');
|
|
7
|
+
*/
|
|
8
|
+
export declare function createLogger(module: string, bindings?: Record<string, unknown>): Logger;
|
|
9
|
+
export type { Logger };
|
|
10
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/core/logger.ts"],"names":[],"mappings":"AAGA,OAAa,EAAE,KAAK,MAAM,EAAE,MAAM,MAAM,CAAC;AAoCzC,eAAO,MAAM,UAAU,EAAE,MAAoD,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,MAAM,CAE3F;AAED,YAAY,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import pino from 'pino';
|
|
5
|
+
/**
|
|
6
|
+
* Log destinations:
|
|
7
|
+
* - Always append NDJSON to `~/.config/opencode/feishu.log` (override with FEISHU_LOG_FILE).
|
|
8
|
+
* - When stdout is a TTY and we're not in daemon mode, also pretty-print to stderr so
|
|
9
|
+
* foreground `opencode-feishu start` still gives a human-readable stream.
|
|
10
|
+
*
|
|
11
|
+
* Level: FEISHU_LOG_LEVEL env (pino levels: fatal/error/warn/info/debug/trace), default info.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_LOG_PATH = join(homedir(), '.config', 'opencode', 'feishu.log');
|
|
14
|
+
const level = process.env.FEISHU_LOG_LEVEL?.toLowerCase() || 'info';
|
|
15
|
+
const logFile = process.env.FEISHU_LOG_FILE || DEFAULT_LOG_PATH;
|
|
16
|
+
const logDir = dirname(logFile);
|
|
17
|
+
if (!existsSync(logDir)) {
|
|
18
|
+
mkdirSync(logDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
const targets = [
|
|
21
|
+
{ target: 'pino/file', level, options: { destination: logFile, mkdir: true } },
|
|
22
|
+
];
|
|
23
|
+
// Mirror to stderr with pretty formatting when attached to a terminal — keeps the
|
|
24
|
+
// foreground `opencode-feishu start` UX unchanged while adding the file sink.
|
|
25
|
+
if (process.stderr.isTTY) {
|
|
26
|
+
targets.push({
|
|
27
|
+
target: 'pino-pretty',
|
|
28
|
+
level,
|
|
29
|
+
options: { destination: 2, colorize: true, singleLine: true, translateTime: 'HH:MM:ss' },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const transport = pino.transport({ targets });
|
|
33
|
+
export const rootLogger = pino({ level, base: undefined }, transport);
|
|
34
|
+
/**
|
|
35
|
+
* Create a child logger with a module tag. Use this in every src/ module:
|
|
36
|
+
* const log = createLogger('FeishuAPI');
|
|
37
|
+
* log.info({ chatId }, 'sending card');
|
|
38
|
+
*/
|
|
39
|
+
export function createLogger(module, bindings = {}) {
|
|
40
|
+
return rootLogger.child({ module, ...bindings });
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/core/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,IAAqB,MAAM,MAAM,CAAC;AAEzC;;;;;;;GAOG;AAEH,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC;AAC9E,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,WAAW,EAAE,IAAI,MAAM,CAAC;AACpE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,gBAAgB,CAAC;AAEhE,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAChC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;IACxB,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,OAAO,GAAU;IACrB,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;CAC/E,CAAC;AAEF,kFAAkF;AAClF,8EAA8E;AAC9E,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACzB,OAAO,CAAC,IAAI,CAAC;QACX,MAAM,EAAE,aAAa;QACrB,KAAK;QACL,OAAO,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE;KACzF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAE9C,MAAM,CAAC,MAAM,UAAU,GAAW,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,SAAS,CAAC,CAAC;AAE9E;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,WAAoC,EAAE;IACjF,OAAO,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FeishuConfig, FeishuMessage } from '../core/types.js';
|
|
2
|
+
import type { SessionManager } from '../core/session-manager.js';
|
|
3
|
+
import type { FeishuAPI } from '../feishu/api.js';
|
|
4
|
+
import type { OpenCodeClient } from '../opencode/client.js';
|
|
5
|
+
export declare class MessageHandler {
|
|
6
|
+
private config;
|
|
7
|
+
private sessionManager;
|
|
8
|
+
private feishuApi;
|
|
9
|
+
private opencode;
|
|
10
|
+
constructor(config: FeishuConfig, sessionManager: SessionManager, feishuApi: FeishuAPI, opencode: OpenCodeClient);
|
|
11
|
+
handleMessage(message: FeishuMessage): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=message-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-handler.d.ts","sourceRoot":"","sources":["../../src/core/message-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAM5D,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,QAAQ,CAAiB;gBAG/B,MAAM,EAAE,YAAY,EACpB,cAAc,EAAE,cAAc,EAC9B,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,cAAc;IAQpB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;CAqI3D"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { FeishuCard } from '../feishu/card.js';
|
|
2
|
+
import { createLogger } from './logger.js';
|
|
3
|
+
const log = createLogger('MessageHandler');
|
|
4
|
+
export class MessageHandler {
|
|
5
|
+
config;
|
|
6
|
+
sessionManager;
|
|
7
|
+
feishuApi;
|
|
8
|
+
opencode;
|
|
9
|
+
constructor(config, sessionManager, feishuApi, opencode) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.sessionManager = sessionManager;
|
|
12
|
+
this.feishuApi = feishuApi;
|
|
13
|
+
this.opencode = opencode;
|
|
14
|
+
}
|
|
15
|
+
async handleMessage(message) {
|
|
16
|
+
try {
|
|
17
|
+
log.info({
|
|
18
|
+
chat_id: message.chat_id,
|
|
19
|
+
chat_type: message.chat_type,
|
|
20
|
+
sender_type: message.sender?.sender_type,
|
|
21
|
+
content: message.content?.substring(0, 100)
|
|
22
|
+
}, 'Received message');
|
|
23
|
+
// 处理 lark-cli 返回的消息格式
|
|
24
|
+
if (!message.sender) {
|
|
25
|
+
log.warn('Message missing sender info, skipping');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Skip messages from the bot itself
|
|
29
|
+
if (message.sender.sender_type === 'app') {
|
|
30
|
+
log.info('Skipping message from app/bot itself');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const chatId = message.chat_id;
|
|
34
|
+
const chatType = message.chat_type;
|
|
35
|
+
log.info({ chatType, sender: message.sender.sender_id?.union_id || 'unknown' }, 'Processing message');
|
|
36
|
+
// Check mention requirement for groups
|
|
37
|
+
if (chatType === 'group' && this.config.requireMention) {
|
|
38
|
+
log.info({ mentions: message.mentions }, 'Checking mentions');
|
|
39
|
+
// 飞书 mention.id 里是机器人的 user 维度 id(open_id/union_id),
|
|
40
|
+
// 不是应用维度的 app_id (cli_*)。用从 /bot/v3/info 拉到的 open_id 比较。
|
|
41
|
+
const botOpenId = this.feishuApi.getBotOpenId();
|
|
42
|
+
const isMentioned = message.mentions?.some(m => !!botOpenId && m.id?.open_id === botOpenId);
|
|
43
|
+
if (!isMentioned) {
|
|
44
|
+
log.info({ chatId }, 'Ignoring message without mention in group');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
log.info('Bot was mentioned');
|
|
48
|
+
}
|
|
49
|
+
// Check group policy
|
|
50
|
+
if (chatType === 'group' && this.config.groupPolicy === 'disabled') {
|
|
51
|
+
log.info('Group messages disabled');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Check allowlist
|
|
55
|
+
if (this.config.allowlist && this.config.allowlist.length > 0) {
|
|
56
|
+
const senderId = message.sender.sender_id?.union_id;
|
|
57
|
+
if (!senderId || !this.config.allowlist.includes(senderId)) {
|
|
58
|
+
log.info({ senderId }, 'Sender not in allowlist');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Get or create session
|
|
63
|
+
log.info({ chatId }, 'Getting or creating session');
|
|
64
|
+
const session = await this.sessionManager.getOrCreateSession(chatId, chatType);
|
|
65
|
+
log.info({ sessionId: session.id, status: session.status }, 'Session ready');
|
|
66
|
+
// Check if session is busy
|
|
67
|
+
if (session.status === 'busy') {
|
|
68
|
+
log.info('Session is busy, sending busy message');
|
|
69
|
+
await this.feishuApi.sendText(chatId, '⏳ 正在处理上一条消息,请稍候...');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Extract text content
|
|
73
|
+
let text = '';
|
|
74
|
+
try {
|
|
75
|
+
const content = JSON.parse(message.content);
|
|
76
|
+
text = content.text || '';
|
|
77
|
+
log.info({ text: text.substring(0, 100) }, 'Extracted text');
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
log.warn({ content: message.content }, 'Failed to parse message content');
|
|
81
|
+
text = '[不支持的消息类型]';
|
|
82
|
+
}
|
|
83
|
+
// Remove @mention from text if present
|
|
84
|
+
if (message.mentions) {
|
|
85
|
+
for (const mention of message.mentions) {
|
|
86
|
+
text = text.replace(mention.key, '').trim();
|
|
87
|
+
}
|
|
88
|
+
log.info({ text: text.substring(0, 100) }, 'Text after removing mentions');
|
|
89
|
+
}
|
|
90
|
+
if (!text) {
|
|
91
|
+
log.info('Empty text content, ignoring');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
log.info({ chatType, chatId, text: text.substring(0, 100) }, 'Message content');
|
|
95
|
+
// Update session status
|
|
96
|
+
this.sessionManager.updateStatus(chatId, 'busy');
|
|
97
|
+
// Send message to OpenCode
|
|
98
|
+
try {
|
|
99
|
+
log.info({ sessionId: session.id }, 'Sending prompt to OpenCode');
|
|
100
|
+
await this.opencode.sendPrompt(session.id, text);
|
|
101
|
+
log.info('Prompt sent successfully');
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
log.error({ err }, 'Failed to send prompt');
|
|
105
|
+
await this.feishuApi.sendCard(chatId, FeishuCard.createErrorCard(`发送消息失败: ${err instanceof Error ? err.message : String(err)}`));
|
|
106
|
+
this.sessionManager.updateStatus(chatId, 'idle');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
log.error({ err }, 'Error handling message');
|
|
111
|
+
try {
|
|
112
|
+
await this.feishuApi.sendText(message.chat_id, '❌ 处理消息时出错,请稍后重试');
|
|
113
|
+
}
|
|
114
|
+
catch (sendErr) {
|
|
115
|
+
log.error({ err: sendErr }, 'Failed to send error message');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=message-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-handler.js","sourceRoot":"","sources":["../../src/core/message-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SessionInfo } from '../core/types.js';
|
|
2
|
+
import type { OpenCodeClient } from '../opencode/client.js';
|
|
3
|
+
export interface SessionManagerOptions {
|
|
4
|
+
/** Override the persistence file path (defaults to ~/.config/opencode/feishu-sessions.json). */
|
|
5
|
+
storagePath?: string;
|
|
6
|
+
/** Set false to disable persistence entirely (tests). Default true. */
|
|
7
|
+
persist?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class SessionManager {
|
|
10
|
+
private sessions;
|
|
11
|
+
private opencode;
|
|
12
|
+
private storagePath;
|
|
13
|
+
private persistEnabled;
|
|
14
|
+
private saveTimer?;
|
|
15
|
+
private pendingSave?;
|
|
16
|
+
constructor(opencode: OpenCodeClient, options?: SessionManagerOptions);
|
|
17
|
+
getOrCreateSession(chatId: string, chatType: 'p2p' | 'group'): Promise<SessionInfo>;
|
|
18
|
+
getSession(chatId: string): SessionInfo | undefined;
|
|
19
|
+
getChatIdBySession(sessionId: string): string | undefined;
|
|
20
|
+
updateStatus(chatId: string, status: 'idle' | 'busy'): void;
|
|
21
|
+
setCurrentMessage(chatId: string, messageId: string): void;
|
|
22
|
+
appendContent(chatId: string, delta: string): void;
|
|
23
|
+
clearCurrentMessage(chatId: string): void;
|
|
24
|
+
/** Drop the mapping for a chat — used when OpenCode session was purged server-side. */
|
|
25
|
+
dropSession(chatId: string): void;
|
|
26
|
+
getAllSessions(): SessionInfo[];
|
|
27
|
+
cleanup(): Promise<void>;
|
|
28
|
+
/** Force immediate persistence of any pending writes (shutdown). */
|
|
29
|
+
flush(): Promise<void>;
|
|
30
|
+
private markDirty;
|
|
31
|
+
private save;
|
|
32
|
+
private restore;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=session-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAoB5D,MAAM,WAAW,qBAAqB;IACpC,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,SAAS,CAAC,CAAiB;IACnC,OAAO,CAAC,WAAW,CAAC,CAAgB;gBAExB,QAAQ,EAAE,cAAc,EAAE,OAAO,GAAE,qBAA0B;IAUnE,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,KAAK,GAAG,OAAO,GACxB,OAAO,CAAC,WAAW,CAAC;IA+BvB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAInD,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IASzD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAO3D,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAQ1D,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQlD,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IASzC,uFAAuF;IACvF,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAOjC,cAAc,IAAI,WAAW,EAAE;IAIzB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAqB9B,oEAAoE;IAC9D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAa5B,OAAO,CAAC,SAAS;YAYH,IAAI;IAuBlB,OAAO,CAAC,OAAO;CAyBhB"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { createLogger } from './logger.js';
|
|
5
|
+
const log = createLogger('SessionManager');
|
|
6
|
+
const DEFAULT_STORAGE_PATH = join(homedir(), '.config', 'opencode', 'feishu-sessions.json');
|
|
7
|
+
const PERSIST_DEBOUNCE_MS = 500;
|
|
8
|
+
const STORAGE_VERSION = 1;
|
|
9
|
+
export class SessionManager {
|
|
10
|
+
sessions = new Map();
|
|
11
|
+
opencode;
|
|
12
|
+
storagePath;
|
|
13
|
+
persistEnabled;
|
|
14
|
+
saveTimer;
|
|
15
|
+
pendingSave;
|
|
16
|
+
constructor(opencode, options = {}) {
|
|
17
|
+
this.opencode = opencode;
|
|
18
|
+
this.storagePath = options.storagePath ?? DEFAULT_STORAGE_PATH;
|
|
19
|
+
this.persistEnabled = options.persist !== false;
|
|
20
|
+
if (this.persistEnabled) {
|
|
21
|
+
this.restore();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async getOrCreateSession(chatId, chatType) {
|
|
25
|
+
const existing = this.sessions.get(chatId);
|
|
26
|
+
if (existing) {
|
|
27
|
+
// Reconcile against OpenCode: if the session was deleted server-side
|
|
28
|
+
// (e.g. user purged via OpenCode CLI), drop the mapping and recreate
|
|
29
|
+
// rather than failing on first sendPrompt with a 404.
|
|
30
|
+
const alive = await this.opencode.sessionExists(existing.id);
|
|
31
|
+
if (alive)
|
|
32
|
+
return existing;
|
|
33
|
+
log.warn({ chatId, sessionId: existing.id }, 'Persisted session no longer exists in OpenCode, recreating');
|
|
34
|
+
this.sessions.delete(chatId);
|
|
35
|
+
}
|
|
36
|
+
const session = await this.opencode.createSession(`Feishu ${chatType} ${chatId}`);
|
|
37
|
+
const info = {
|
|
38
|
+
id: session.id,
|
|
39
|
+
chatId,
|
|
40
|
+
chatType,
|
|
41
|
+
status: 'idle',
|
|
42
|
+
};
|
|
43
|
+
this.sessions.set(chatId, info);
|
|
44
|
+
log.info({ chatId, chatType, sessionId: session.id }, 'Created new session');
|
|
45
|
+
this.markDirty();
|
|
46
|
+
return info;
|
|
47
|
+
}
|
|
48
|
+
getSession(chatId) {
|
|
49
|
+
return this.sessions.get(chatId);
|
|
50
|
+
}
|
|
51
|
+
getChatIdBySession(sessionId) {
|
|
52
|
+
for (const [chatId, session] of this.sessions.entries()) {
|
|
53
|
+
if (session.id === sessionId) {
|
|
54
|
+
return chatId;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
updateStatus(chatId, status) {
|
|
60
|
+
const session = this.sessions.get(chatId);
|
|
61
|
+
if (session) {
|
|
62
|
+
session.status = status;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
setCurrentMessage(chatId, messageId) {
|
|
66
|
+
const session = this.sessions.get(chatId);
|
|
67
|
+
if (session) {
|
|
68
|
+
session.currentMessageId = messageId;
|
|
69
|
+
session.lastUpdateTime = Date.now();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
appendContent(chatId, delta) {
|
|
73
|
+
const session = this.sessions.get(chatId);
|
|
74
|
+
if (session) {
|
|
75
|
+
session.currentContent = (session.currentContent || '') + delta;
|
|
76
|
+
session.lastUpdateTime = Date.now();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
clearCurrentMessage(chatId) {
|
|
80
|
+
const session = this.sessions.get(chatId);
|
|
81
|
+
if (session) {
|
|
82
|
+
session.currentMessageId = undefined;
|
|
83
|
+
session.currentContent = undefined;
|
|
84
|
+
session.lastUpdateTime = undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Drop the mapping for a chat — used when OpenCode session was purged server-side. */
|
|
88
|
+
dropSession(chatId) {
|
|
89
|
+
if (this.sessions.delete(chatId)) {
|
|
90
|
+
log.info({ chatId }, 'Dropped stale session mapping');
|
|
91
|
+
this.markDirty();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
getAllSessions() {
|
|
95
|
+
return Array.from(this.sessions.values());
|
|
96
|
+
}
|
|
97
|
+
async cleanup() {
|
|
98
|
+
log.info({ sessionCount: this.sessions.size }, 'Cleaning up sessions');
|
|
99
|
+
// Flush first — must persist the chat_id→session_id mapping BEFORE
|
|
100
|
+
// clearing in-memory state, otherwise flush() would save an empty list.
|
|
101
|
+
await this.flush();
|
|
102
|
+
for (const [, session] of this.sessions.entries()) {
|
|
103
|
+
try {
|
|
104
|
+
if (session.status === 'busy') {
|
|
105
|
+
await this.opencode.abortSession(session.id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
log.error({ err, sessionId: session.id }, 'Failed to cleanup session');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Process is exiting; drop in-memory state. Next start restores from disk.
|
|
113
|
+
this.sessions.clear();
|
|
114
|
+
}
|
|
115
|
+
/** Force immediate persistence of any pending writes (shutdown). */
|
|
116
|
+
async flush() {
|
|
117
|
+
if (this.saveTimer) {
|
|
118
|
+
clearTimeout(this.saveTimer);
|
|
119
|
+
this.saveTimer = undefined;
|
|
120
|
+
}
|
|
121
|
+
if (this.pendingSave) {
|
|
122
|
+
await this.pendingSave;
|
|
123
|
+
}
|
|
124
|
+
await this.save();
|
|
125
|
+
}
|
|
126
|
+
// -------- persistence internals --------
|
|
127
|
+
markDirty() {
|
|
128
|
+
if (!this.persistEnabled)
|
|
129
|
+
return;
|
|
130
|
+
if (this.saveTimer)
|
|
131
|
+
clearTimeout(this.saveTimer);
|
|
132
|
+
this.saveTimer = setTimeout(() => {
|
|
133
|
+
this.saveTimer = undefined;
|
|
134
|
+
this.pendingSave = this.save().catch(err => {
|
|
135
|
+
log.error({ err }, 'Background persist failed');
|
|
136
|
+
});
|
|
137
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
138
|
+
}
|
|
139
|
+
async save() {
|
|
140
|
+
if (!this.persistEnabled)
|
|
141
|
+
return;
|
|
142
|
+
const state = {
|
|
143
|
+
version: STORAGE_VERSION,
|
|
144
|
+
sessions: Array.from(this.sessions.values()).map(s => ({
|
|
145
|
+
chatId: s.chatId,
|
|
146
|
+
chatType: s.chatType,
|
|
147
|
+
sessionId: s.id,
|
|
148
|
+
})),
|
|
149
|
+
};
|
|
150
|
+
const dir = dirname(this.storagePath);
|
|
151
|
+
if (!existsSync(dir))
|
|
152
|
+
mkdirSync(dir, { recursive: true });
|
|
153
|
+
// Atomic-ish write: write to tmp + rename.
|
|
154
|
+
const tmp = `${this.storagePath}.tmp`;
|
|
155
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
156
|
+
const { renameSync } = await import('fs');
|
|
157
|
+
renameSync(tmp, this.storagePath);
|
|
158
|
+
log.debug({ path: this.storagePath, count: state.sessions.length }, 'Persisted sessions');
|
|
159
|
+
}
|
|
160
|
+
restore() {
|
|
161
|
+
if (!existsSync(this.storagePath))
|
|
162
|
+
return;
|
|
163
|
+
try {
|
|
164
|
+
const raw = readFileSync(this.storagePath, 'utf-8');
|
|
165
|
+
const parsed = JSON.parse(raw);
|
|
166
|
+
if (parsed.version !== STORAGE_VERSION || !Array.isArray(parsed.sessions)) {
|
|
167
|
+
log.warn({ path: this.storagePath, version: parsed.version }, 'Unknown session file version, starting fresh');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
for (const s of parsed.sessions) {
|
|
171
|
+
if (!s?.chatId || !s?.sessionId || !s?.chatType)
|
|
172
|
+
continue;
|
|
173
|
+
this.sessions.set(s.chatId, {
|
|
174
|
+
id: s.sessionId,
|
|
175
|
+
chatId: s.chatId,
|
|
176
|
+
chatType: s.chatType,
|
|
177
|
+
status: 'idle',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
log.info({ count: this.sessions.size, path: this.storagePath }, 'Restored sessions from disk');
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
log.warn({ err, path: this.storagePath }, 'Failed to restore sessions, starting fresh');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=session-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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"}
|