@router-agent/channel 0.0.0-stage → 0.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +12 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -0
- package/dist/feishu/index.js +146 -0
- package/dist/feishu/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/ingress.d.ts +46 -0
- package/dist/ingress.js +90 -0
- package/dist/ingress.js.map +1 -0
- package/dist/legacy.js +26 -0
- package/dist/legacy.js.map +1 -0
- package/dist/plugins/loader.d.ts +33 -0
- package/dist/plugins/loader.js +79 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/types.d.ts +30 -0
- package/dist/plugins/types.js +2 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/port.d.ts +111 -0
- package/dist/port.js +8 -0
- package/dist/port.js.map +1 -0
- package/dist/registry.d.ts +21 -0
- package/dist/registry.js +50 -0
- package/dist/registry.js.map +1 -0
- package/dist/router.d.ts +24 -0
- package/dist/router.js +36 -0
- package/dist/router.js.map +1 -0
- package/dist/sources/feishu-bot-admin.js +119 -0
- package/dist/sources/feishu-bot-admin.js.map +1 -0
- package/dist/sources/feishu-directory.js +77 -0
- package/dist/sources/feishu-directory.js.map +1 -0
- package/dist/sources/feishu-identity.js +646 -0
- package/dist/sources/feishu-identity.js.map +1 -0
- package/dist/sources/feishu-image.js +338 -0
- package/dist/sources/feishu-image.js.map +1 -0
- package/dist/sources/feishu.js +645 -0
- package/dist/sources/feishu.js.map +1 -0
- package/dist/sources/lark.js +214 -0
- package/dist/sources/lark.js.map +1 -0
- package/package.json +35 -4
- package/README.md +0 -3
package/dist/port.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Trigger } from '@router-agent/core/model.js';
|
|
2
|
+
/**
|
|
3
|
+
* 默认通道实例 ID。
|
|
4
|
+
*
|
|
5
|
+
* 一期只有飞书一个通道,所有历史数据都归属于这个实例;等接入第二个通道时,
|
|
6
|
+
* 实例 ID 会来自配置(`channels.<id>`),这里的常量只作为兼容默认值。
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_CHANNEL_ID = "feishu";
|
|
9
|
+
/** 通道能力声明。内核按能力降级,不假设所有通道长得一样。 */
|
|
10
|
+
export interface ChannelCapabilities {
|
|
11
|
+
/** 支持话题/线程内回复。 */
|
|
12
|
+
threadReply: boolean;
|
|
13
|
+
/** 支持消息回执(表情或已读)。 */
|
|
14
|
+
ack: boolean;
|
|
15
|
+
/** 支持富文本渲染。 */
|
|
16
|
+
richText: boolean;
|
|
17
|
+
/** 支持创建可分享的长文档。 */
|
|
18
|
+
documents: boolean;
|
|
19
|
+
/** 支持枚举可接收目标。 */
|
|
20
|
+
directory: boolean;
|
|
21
|
+
/** 支持拉取入站附件。 */
|
|
22
|
+
attachmentsIn: boolean;
|
|
23
|
+
/** 支持发送出站附件(一期全部 false)。 */
|
|
24
|
+
attachmentsOut: boolean;
|
|
25
|
+
/** 支持服务端幂等键。 */
|
|
26
|
+
idempotency: boolean;
|
|
27
|
+
/** 单条消息字符上限,用于切块。 */
|
|
28
|
+
maxTextLength: number;
|
|
29
|
+
/** 私聊是否可用。 */
|
|
30
|
+
directMessage: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** 出站消息的中立形态;具体渲染由通道适配器负责。 */
|
|
33
|
+
export interface OutboundMessage {
|
|
34
|
+
channelId: string;
|
|
35
|
+
targetId: string;
|
|
36
|
+
/** 希望回复到的话题根;为空时发到 targetId。 */
|
|
37
|
+
threadRef?: string;
|
|
38
|
+
text: string;
|
|
39
|
+
format: 'plain' | 'markdown';
|
|
40
|
+
idempotencyKey: string;
|
|
41
|
+
/** 触发这条回复的用户消息,用于发送成功后撤销回执。 */
|
|
42
|
+
originMessageId?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface SendReceipt {
|
|
45
|
+
messageId?: string;
|
|
46
|
+
/** 实际发生的降级,便于日志与排障。 */
|
|
47
|
+
degradedTo?: 'plain' | 'rich' | 'document';
|
|
48
|
+
}
|
|
49
|
+
/** 归一化结果:要么得到领域 Trigger,要么给出丢弃原因。 */
|
|
50
|
+
export interface NormalizedInbound {
|
|
51
|
+
trigger?: Trigger;
|
|
52
|
+
dropped?: string;
|
|
53
|
+
targetId?: string;
|
|
54
|
+
targetName?: string;
|
|
55
|
+
actorId?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ChannelStatus {
|
|
58
|
+
appId?: string;
|
|
59
|
+
bot?: {
|
|
60
|
+
available?: boolean;
|
|
61
|
+
status?: string;
|
|
62
|
+
message?: string;
|
|
63
|
+
};
|
|
64
|
+
user?: {
|
|
65
|
+
available?: boolean;
|
|
66
|
+
status?: string;
|
|
67
|
+
message?: string;
|
|
68
|
+
openId?: string;
|
|
69
|
+
userName?: string;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export interface ChannelAdmin {
|
|
73
|
+
readonly profileHome: string;
|
|
74
|
+
status(): Promise<ChannelStatus>;
|
|
75
|
+
configure(appId: string, appSecret: string): Promise<{
|
|
76
|
+
ok: boolean;
|
|
77
|
+
message?: string;
|
|
78
|
+
}>;
|
|
79
|
+
startLogin(): Promise<any>;
|
|
80
|
+
completeLogin(code: string): Promise<any>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 通道公共契约。
|
|
84
|
+
*
|
|
85
|
+
* 内核只依赖这个接口;飞书的 lark-cli 调用、事件过滤、卡片渲染全部封装在实现里。
|
|
86
|
+
* 一期只实现飞书,但签名预留了话题、回执、文档与附件能力,接第二通道时不用改内核。
|
|
87
|
+
*/
|
|
88
|
+
export interface ChannelAdapter {
|
|
89
|
+
readonly id: string;
|
|
90
|
+
readonly kind: string;
|
|
91
|
+
readonly capabilities: ChannelCapabilities;
|
|
92
|
+
/** 启动入站消费;主动拉取型通道可以不实现。 */
|
|
93
|
+
start?(sink: (raw: unknown) => void): Promise<void> | void;
|
|
94
|
+
stop?(): void;
|
|
95
|
+
/** 原始事件 -> 领域 Trigger;只做传输合法性,不做需要跨消息的富化。 */
|
|
96
|
+
normalize(raw: unknown): NormalizedInbound;
|
|
97
|
+
/** 这条消息入库前是否需要富化(引用回查、附件下载/OCR)。 */
|
|
98
|
+
needsEnrichment?(trigger: Trigger): boolean;
|
|
99
|
+
/** 通道内富化;失败不应阻塞主流程,实现方负责降级与记日志。 */
|
|
100
|
+
enrich?(trigger: Trigger): Promise<Trigger>;
|
|
101
|
+
send(message: OutboundMessage): Promise<SendReceipt>;
|
|
102
|
+
/** 给一条消息加回执,返回通道侧句柄;不支持时返回 null。 */
|
|
103
|
+
ack?(messageId: string): Promise<string | null>;
|
|
104
|
+
clearAck?(messageId: string, handle: string): Promise<void>;
|
|
105
|
+
listTargets?(): Promise<{
|
|
106
|
+
id: string;
|
|
107
|
+
name: string;
|
|
108
|
+
}[]>;
|
|
109
|
+
createDocument?(title: string, markdown: string): Promise<string>;
|
|
110
|
+
readonly admin?: ChannelAdmin;
|
|
111
|
+
}
|
package/dist/port.js
ADDED
package/dist/port.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port.js","sourceRoot":"","sources":["../../../../packages/channel/src/port.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAQ,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Logger } from '@router-agent/infra/logger.js';
|
|
2
|
+
import type { ChannelAdapter, ChannelCapabilities } from './port.js';
|
|
3
|
+
/**
|
|
4
|
+
* 通道注册表:按实例 ID 管理适配器。
|
|
5
|
+
*
|
|
6
|
+
* 一期只注册飞书;接第二通道时只需在这里多注册一个实现,内核代码不动。
|
|
7
|
+
*/
|
|
8
|
+
export declare class ChannelRegistry {
|
|
9
|
+
private readonly adapters;
|
|
10
|
+
private readonly logger;
|
|
11
|
+
constructor(logger?: Logger);
|
|
12
|
+
register(adapter: ChannelAdapter): void;
|
|
13
|
+
get(id: string): ChannelAdapter | undefined;
|
|
14
|
+
/** 未注册属于永久失败:重试也不会变好,交给调用方决定是否进死信。 */
|
|
15
|
+
require(id: string): ChannelAdapter;
|
|
16
|
+
capabilities(id: string): ChannelCapabilities;
|
|
17
|
+
list(): ChannelAdapter[];
|
|
18
|
+
/** 依次启动所有实现 `start` 的适配器,并把原始事件标上通道 ID。 */
|
|
19
|
+
startAll(sink: (channelId: string, raw: unknown) => void): Promise<void>;
|
|
20
|
+
stopAll(): void;
|
|
21
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { noopLogger } from '@router-agent/infra/logger.js';
|
|
2
|
+
import { ChannelError } from './errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* 通道注册表:按实例 ID 管理适配器。
|
|
5
|
+
*
|
|
6
|
+
* 一期只注册飞书;接第二通道时只需在这里多注册一个实现,内核代码不动。
|
|
7
|
+
*/
|
|
8
|
+
export class ChannelRegistry {
|
|
9
|
+
adapters = new Map();
|
|
10
|
+
logger;
|
|
11
|
+
constructor(logger = noopLogger) {
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
register(adapter) {
|
|
15
|
+
if (this.adapters.has(adapter.id)) {
|
|
16
|
+
this.logger.warn('channel:duplicate-id', { channel: adapter.id, kind: adapter.kind });
|
|
17
|
+
}
|
|
18
|
+
this.adapters.set(adapter.id, adapter);
|
|
19
|
+
}
|
|
20
|
+
get(id) {
|
|
21
|
+
return this.adapters.get(id);
|
|
22
|
+
}
|
|
23
|
+
/** 未注册属于永久失败:重试也不会变好,交给调用方决定是否进死信。 */
|
|
24
|
+
require(id) {
|
|
25
|
+
const adapter = this.adapters.get(id);
|
|
26
|
+
if (!adapter) {
|
|
27
|
+
throw new ChannelError(id, 'channel:not-registered', `通道未注册: ${id}`, false);
|
|
28
|
+
}
|
|
29
|
+
return adapter;
|
|
30
|
+
}
|
|
31
|
+
capabilities(id) {
|
|
32
|
+
return this.require(id).capabilities;
|
|
33
|
+
}
|
|
34
|
+
list() {
|
|
35
|
+
return [...this.adapters.values()];
|
|
36
|
+
}
|
|
37
|
+
/** 依次启动所有实现 `start` 的适配器,并把原始事件标上通道 ID。 */
|
|
38
|
+
async startAll(sink) {
|
|
39
|
+
for (const adapter of this.adapters.values()) {
|
|
40
|
+
if (!adapter.start)
|
|
41
|
+
continue;
|
|
42
|
+
await adapter.start(raw => sink(adapter.id, raw));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
stopAll() {
|
|
46
|
+
for (const adapter of this.adapters.values())
|
|
47
|
+
adapter.stop?.();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../../packages/channel/src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAe,MAAM,+BAA+B,CAAA;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAG1C;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACT,QAAQ,GAAG,IAAI,GAAG,EAA0B,CAAA;IAC5C,MAAM,CAAQ;IAE/B,YAAY,SAAiB,UAAU;QACrC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;IAED,QAAQ,CAAC,OAAuB;QAC9B,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;QACvF,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IACxC,CAAC;IAED,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC9B,CAAC;IAED,sCAAsC;IACtC,OAAO,CAAC,EAAU;QAChB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACrC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,YAAY,CAAC,EAAE,EAAE,wBAAwB,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,CAAC,CAAA;QAC7E,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,YAAY,CAAC,EAAU;QACrB,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,YAAY,CAAA;IACtC,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;IACpC,CAAC;IAED,2CAA2C;IAC3C,KAAK,CAAC,QAAQ,CAAC,IAA+C;QAC5D,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7C,IAAI,CAAC,OAAO,CAAC,KAAK;gBAAE,SAAQ;YAC5B,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAA;QACnD,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;YAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAA;IAChE,CAAC;CACF"}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Logger } from '@router-agent/infra/logger.js';
|
|
2
|
+
import type { ChannelRegistry } from './registry.js';
|
|
3
|
+
/** Outbox 交给通道层的最小字段集;与 store 的 OutboxItem 结构兼容。 */
|
|
4
|
+
export interface ChannelOutboxItem {
|
|
5
|
+
channelKind: string;
|
|
6
|
+
channelId: string;
|
|
7
|
+
threadId: string;
|
|
8
|
+
text: string;
|
|
9
|
+
key: string;
|
|
10
|
+
originMessageId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 出站分发:按通道实例 ID 找到适配器,把中立消息交给它渲染与发送。
|
|
14
|
+
*
|
|
15
|
+
* 一期 `channelKind` 仍然是 `'feishu'`,与实例 ID 相同;接第二通道时
|
|
16
|
+
* 由配置把实例 ID 与实现 ID 分开。未知通道按旧行为跳过并记日志,
|
|
17
|
+
* 避免历史脏数据把 Outbox 卡死。
|
|
18
|
+
*/
|
|
19
|
+
export declare class ChannelRouter {
|
|
20
|
+
private readonly registry;
|
|
21
|
+
private readonly logger;
|
|
22
|
+
constructor(registry: ChannelRegistry, logger?: Logger);
|
|
23
|
+
send(item: ChannelOutboxItem): Promise<void>;
|
|
24
|
+
}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { noopLogger } from '@router-agent/infra/logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* 出站分发:按通道实例 ID 找到适配器,把中立消息交给它渲染与发送。
|
|
4
|
+
*
|
|
5
|
+
* 一期 `channelKind` 仍然是 `'feishu'`,与实例 ID 相同;接第二通道时
|
|
6
|
+
* 由配置把实例 ID 与实现 ID 分开。未知通道按旧行为跳过并记日志,
|
|
7
|
+
* 避免历史脏数据把 Outbox 卡死。
|
|
8
|
+
*/
|
|
9
|
+
export class ChannelRouter {
|
|
10
|
+
registry;
|
|
11
|
+
logger;
|
|
12
|
+
constructor(registry, logger = noopLogger) {
|
|
13
|
+
this.registry = registry;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
}
|
|
16
|
+
async send(item) {
|
|
17
|
+
const text = item.text.trim();
|
|
18
|
+
if (!text)
|
|
19
|
+
return;
|
|
20
|
+
const adapter = this.registry.get(item.channelKind);
|
|
21
|
+
if (!adapter) {
|
|
22
|
+
this.logger.warn('channel:not-registered', { channel: item.channelKind, target: item.channelId, key: item.key });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await adapter.send({
|
|
26
|
+
channelId: item.channelKind,
|
|
27
|
+
targetId: item.channelId,
|
|
28
|
+
...(item.threadId ? { threadRef: item.threadId } : {}),
|
|
29
|
+
text,
|
|
30
|
+
format: 'markdown',
|
|
31
|
+
idempotencyKey: item.key,
|
|
32
|
+
...(item.originMessageId ? { originMessageId: item.originMessageId } : {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../../../packages/channel/src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAe,MAAM,+BAA+B,CAAA;AAavE;;;;;;GAMG;AACH,MAAM,OAAO,aAAa;IAEL;IACA;IAFnB,YACmB,QAAyB,EACzB,SAAiB,UAAU;QAD3B,aAAQ,GAAR,QAAQ,CAAiB;QACzB,WAAM,GAAN,MAAM,CAAqB;IAC3C,CAAC;IAEJ,KAAK,CAAC,IAAI,CAAC,IAAuB;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;QAC7B,IAAI,CAAC,IAAI;YAAE,OAAM;QACjB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QACnD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,wBAAwB,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;YAChH,OAAM;QACR,CAAC;QACD,MAAM,OAAO,CAAC,IAAI,CAAC;YACjB,SAAS,EAAE,IAAI,CAAC,WAAW;YAC3B,QAAQ,EAAE,IAAI,CAAC,SAAS;YACxB,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACtD,IAAI;YACJ,MAAM,EAAE,UAAU;YAClB,cAAc,EAAE,IAAI,CAAC,GAAG;YACxB,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3E,CAAC,CAAA;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { LARK_PROFILE_HOME, parseAuthStatus, parseDeviceLogin, } from './lark.js';
|
|
3
|
+
/**
|
|
4
|
+
* router-agent 实际用到的飞书业务域。
|
|
5
|
+
*
|
|
6
|
+
* 不要用默认的「全部域」授权:router-agent 只需要 IM(消息/群/成员)、
|
|
7
|
+
* 云文档、云空间与通讯录,避免用户被要求授予日历、邮箱、审批等无关权限。
|
|
8
|
+
*/
|
|
9
|
+
export const FEISHU_AUTH_DOMAINS = ['im', 'docs', 'drive', 'contact'];
|
|
10
|
+
/**
|
|
11
|
+
* 业务域之外必须显式申请的 scope。
|
|
12
|
+
*
|
|
13
|
+
* lark-cli 的 `contact` 域只映射到 `contact:user.basic_profile:readonly` 与
|
|
14
|
+
* `contact:user:search`,**不含** `contact:user.id:readonly`。而 router-agent 要用
|
|
15
|
+
* `contact/v3/users/batch_get_id` 把允许名单里的邮箱解析成 open_id,这个接口要的正是
|
|
16
|
+
* `contact:user.id:readonly`:域映射不覆盖它 → 用户授权成功、token 里却没有这个权限,
|
|
17
|
+
* 表现成「用户已经授权好飞书,但没有该权限」。
|
|
18
|
+
*
|
|
19
|
+
* lark-cli 的 `--scope` 与 `--domain` 是叠加关系,所以在这里补一个显式 scope:
|
|
20
|
+
* 用户在授权页会看到这一项,授权后应用侧也一并开通。
|
|
21
|
+
*/
|
|
22
|
+
export const FEISHU_AUTH_SCOPES = ['contact:user.id:readonly'];
|
|
23
|
+
/** 授权参数:业务域 + 显式 scope(lark-cli 两者叠加)。 */
|
|
24
|
+
export function feishuAuthScopeArgs() {
|
|
25
|
+
return ['--domain', FEISHU_AUTH_DOMAINS.join(','), '--scope', FEISHU_AUTH_SCOPES.join(',')];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 飞书机器人凭据与授权管理。
|
|
29
|
+
*
|
|
30
|
+
* 所有命令都通过已绑定固定 profile 的 lark-cli runner 执行:
|
|
31
|
+
* - App Secret 只走 stdin,不写 config.yaml,也不进 argv / 日志;
|
|
32
|
+
* - 授权走 Device Flow,先拿 verification_url,再由用户确认后完成轮询。
|
|
33
|
+
*/
|
|
34
|
+
export class FeishuBotAdmin {
|
|
35
|
+
lark;
|
|
36
|
+
profileHome;
|
|
37
|
+
constructor(opts) {
|
|
38
|
+
this.lark = opts.lark;
|
|
39
|
+
this.profileHome = opts.profileHome ?? LARK_PROFILE_HOME;
|
|
40
|
+
}
|
|
41
|
+
/** 把 App ID / App Secret 写入 lark-cli 的加密存储并校验。 */
|
|
42
|
+
async configure(appId, appSecret) {
|
|
43
|
+
const id = appId.trim();
|
|
44
|
+
const secret = appSecret.trim();
|
|
45
|
+
if (!id)
|
|
46
|
+
return { ok: false, message: 'App ID 不能为空' };
|
|
47
|
+
if (!secret)
|
|
48
|
+
return { ok: false, message: 'App Secret 不能为空' };
|
|
49
|
+
try {
|
|
50
|
+
mkdirSync(this.profileHome, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return { ok: false, message: `无法创建 lark-cli profile 目录:${errorMessage(error)}` };
|
|
54
|
+
}
|
|
55
|
+
const res = await this.lark.run(['config', 'init', '--app-id', id, '--app-secret-stdin'], { input: `${secret}\n`, timeoutMs: 30_000 });
|
|
56
|
+
const detail = redact(`${res.stdout}\n${res.stderr}`, secret);
|
|
57
|
+
if (res.code !== 0) {
|
|
58
|
+
return { ok: false, message: detail || `lark-cli 退出码 ${res.code}` };
|
|
59
|
+
}
|
|
60
|
+
if (!/configuration saved|配置已保存/i.test(res.stdout)) {
|
|
61
|
+
return { ok: false, message: detail || 'lark-cli 未确认配置已保存' };
|
|
62
|
+
}
|
|
63
|
+
return { ok: true, message: 'App Secret 已写入 lark-cli 加密存储' };
|
|
64
|
+
}
|
|
65
|
+
/** 读取机器人身份与用户授权状态。 */
|
|
66
|
+
async status() {
|
|
67
|
+
try {
|
|
68
|
+
const res = await this.lark.run(['auth', 'status', '--json']);
|
|
69
|
+
if (res.code !== 0)
|
|
70
|
+
return this.emptyStatus(redact(`${res.stdout}\n${res.stderr}`));
|
|
71
|
+
return parseAuthStatus(res.stdout);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return this.emptyStatus(errorMessage(error));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** 发起 Device Flow 授权,返回验证链接与 device code。 */
|
|
78
|
+
async startLogin() {
|
|
79
|
+
const res = await this.lark.run(['auth', 'login', '--no-wait', '--json', ...feishuAuthScopeArgs()], { timeoutMs: 30_000 });
|
|
80
|
+
if (res.code !== 0) {
|
|
81
|
+
throw new Error(redact(`${res.stdout}\n${res.stderr}`) || `lark-cli 退出码 ${res.code}`);
|
|
82
|
+
}
|
|
83
|
+
const parsed = parseDeviceLogin(res.stdout, res.stderr);
|
|
84
|
+
if (!parsed.deviceCode) {
|
|
85
|
+
throw new Error(`lark-cli 未返回 device code:${redact(`${res.stdout}\n${res.stderr}`) || '无输出'}`);
|
|
86
|
+
}
|
|
87
|
+
if (!parsed.verificationUrl) {
|
|
88
|
+
throw new Error(`lark-cli 未返回验证链接:${redact(`${res.stdout}\n${res.stderr}`) || '无输出'}`);
|
|
89
|
+
}
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
/** 完成授权轮询;最长等待 3 分钟。 */
|
|
93
|
+
async completeLogin(deviceCode) {
|
|
94
|
+
const code = deviceCode.trim();
|
|
95
|
+
if (!code)
|
|
96
|
+
throw new Error('device code 不能为空');
|
|
97
|
+
const res = await this.lark.run(['auth', 'login', '--device-code', code, '--json'], { timeoutMs: 180_000 });
|
|
98
|
+
if (res.code !== 0) {
|
|
99
|
+
throw new Error(redact(`${res.stdout}\n${res.stderr}`) || `lark-cli 退出码 ${res.code}`);
|
|
100
|
+
}
|
|
101
|
+
return this.status();
|
|
102
|
+
}
|
|
103
|
+
emptyStatus(message) {
|
|
104
|
+
return {
|
|
105
|
+
appId: '',
|
|
106
|
+
bot: { available: false, status: '', message },
|
|
107
|
+
user: { available: false, status: '', message: '', openId: '', userName: '' },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function errorMessage(error) {
|
|
112
|
+
return error instanceof Error ? error.message : String(error);
|
|
113
|
+
}
|
|
114
|
+
/** 截断并去掉可能的敏感值,避免错误响应里回显 App Secret。 */
|
|
115
|
+
function redact(text, secret = '') {
|
|
116
|
+
const cleaned = secret ? text.split(secret).join('***') : text;
|
|
117
|
+
return cleaned.trim().slice(0, 240);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=feishu-bot-admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feishu-bot-admin.js","sourceRoot":"","sources":["../../../../../packages/channel/src/sources/feishu-bot-admin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { noopLogger, errorMessage } from '@router-agent/infra/logger.js';
|
|
3
|
+
/** 群名目录:定期从 lark-cli 拉取并缓存,用于面板与通知里的可读名称。 */
|
|
4
|
+
export class ChatDirectory {
|
|
5
|
+
file;
|
|
6
|
+
logger;
|
|
7
|
+
names = new Map();
|
|
8
|
+
constructor(file, logger = noopLogger) {
|
|
9
|
+
this.file = file;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
this.load();
|
|
12
|
+
}
|
|
13
|
+
nameOf(chatId) {
|
|
14
|
+
return this.names.get(chatId) ?? '';
|
|
15
|
+
}
|
|
16
|
+
entries() {
|
|
17
|
+
return [...this.names.entries()].map(([chatId, name]) => ({ chatId, name }));
|
|
18
|
+
}
|
|
19
|
+
/** 用 lark-cli 的真实返回刷新缓存;失败保留旧值。 */
|
|
20
|
+
async refresh(runner) {
|
|
21
|
+
const res = await runner.run(['im', '+chat-list', '--as', 'bot', '--format', 'json']);
|
|
22
|
+
if (res.code !== 0) {
|
|
23
|
+
this.logger.warn('chats:refresh-failed', { code: res.code, message: res.stderr.slice(0, 200) });
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
const parsed = parseChatList(res.stdout, this.logger);
|
|
27
|
+
for (const entry of parsed)
|
|
28
|
+
this.names.set(entry.chatId, entry.name);
|
|
29
|
+
this.save();
|
|
30
|
+
return parsed.length;
|
|
31
|
+
}
|
|
32
|
+
load() {
|
|
33
|
+
if (!existsSync(this.file))
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
const doc = JSON.parse(readFileSync(this.file, 'utf8'));
|
|
37
|
+
for (const entry of doc.chats ?? [])
|
|
38
|
+
this.names.set(entry.chatId, entry.name);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// 缓存损坏时当作空,但留一条日志,避免群名一直显示为空却查不到原因。
|
|
42
|
+
this.logger.warn('chats:cache-invalid', { path: this.file, message: errorMessage(error) });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
save() {
|
|
46
|
+
writeFileSync(this.file, `${JSON.stringify({ chats: this.entries() }, null, 2)}\n`, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** 兼容 `{data:{chats:[...]}}` / `{data:{items:[...]}}` / 裸数组。 */
|
|
50
|
+
export function parseChatList(stdout, logger = noopLogger) {
|
|
51
|
+
let json;
|
|
52
|
+
try {
|
|
53
|
+
json = JSON.parse(stdout);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (stdout.trim())
|
|
57
|
+
logger.warn('chats:parse-failed', { message: errorMessage(error), sample: stdout.slice(0, 200) });
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(json))
|
|
61
|
+
return normalize(json);
|
|
62
|
+
const root = json;
|
|
63
|
+
const data = root?.data ?? root;
|
|
64
|
+
const list = data?.chats ?? data?.items ?? [];
|
|
65
|
+
return Array.isArray(list) ? normalize(list) : [];
|
|
66
|
+
}
|
|
67
|
+
function normalize(list) {
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const raw of list) {
|
|
70
|
+
const chatId = String(raw.chat_id ?? raw.chatId ?? '');
|
|
71
|
+
const name = String(raw.name ?? raw.chat_name ?? '');
|
|
72
|
+
if (chatId)
|
|
73
|
+
out.push({ chatId, name });
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=feishu-directory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feishu-directory.js","sourceRoot":"","sources":["../../../../../packages/channel/src/sources/feishu-directory.ts"],"names":[],"mappings":"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"}
|