feishu-user-plugin 1.3.3 → 1.3.5

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.
@@ -0,0 +1,151 @@
1
+ // Unified Feishu ID resolver — accepts three input forms and produces
2
+ // a concrete { obj_type, obj_token } pair that downstream docx / bitable
3
+ // tools can consume.
4
+ //
5
+ // Accepted inputs:
6
+ // • Native token (e.g. "doccnXXX", "bascnXXX", "docxAAA")
7
+ // • Wiki node token (e.g. "wikcnAAA", "wikmXXX", "wikxxxXXX")
8
+ // • Full Feishu URL (e.g. "https://xxx.feishu.cn/docx/DocXXX?...")
9
+ //
10
+ // Exposed functions:
11
+ // parseFeishuInput(input) — pure, no I/O. Maps input → { kind, token, raw }.
12
+ // resolveToObj(input, official) — async. For wiki kind, calls official.getWikiNode
13
+ // to unwrap to the underlying obj_token + obj_type.
14
+ // Results cached for 10 min.
15
+
16
+ // Host allows the `<sub>.feishu.cn` / `<sub>.larksuite.com` pattern with an
17
+ // optional port (some corporate proxies surface :443 explicitly; rare but we
18
+ // cost nothing by allowing it).
19
+ const URL_RE = /^https?:\/\/[^/]*(feishu\.cn|larksuite\.com)(?::\d+)?\/(docx|wiki|base|sheets|file|docs)\/([A-Za-z0-9_-]+)/;
20
+ const WIKI_BARE_RE = /^(wik[a-z]{1,4})([A-Za-z0-9_-]{6,})$/; // wikcn / wikm / wikn / wiki prefixes
21
+
22
+ // Feishu URL segment → obj_type mapping. 'docs' is the legacy doc type (pre-docx).
23
+ const URL_KIND_MAP = {
24
+ docx: 'docx',
25
+ docs: 'doc',
26
+ wiki: 'wiki',
27
+ base: 'bitable',
28
+ sheets: 'sheet',
29
+ file: 'file',
30
+ };
31
+
32
+ /**
33
+ * Parse a Feishu input string into its components. Pure / no I/O.
34
+ * @param {string} input
35
+ * @returns {{kind: string, token: string, raw: string}}
36
+ * kind: 'docx' | 'doc' | 'wiki' | 'bitable' | 'sheet' | 'file' | 'raw'
37
+ * token: the extracted token (or the original string if kind='raw')
38
+ * raw: the original input (for diagnostics)
39
+ */
40
+ function parseFeishuInput(input) {
41
+ if (input === null || input === undefined) {
42
+ throw new Error('parseFeishuInput: input is required');
43
+ }
44
+ const s = String(input).trim();
45
+ if (!s) throw new Error('parseFeishuInput: input is empty');
46
+
47
+ // URL form
48
+ const m = s.match(URL_RE);
49
+ if (m) {
50
+ const segment = m[2];
51
+ const token = m[3];
52
+ const kind = URL_KIND_MAP[segment] || 'raw';
53
+ return { kind, token, raw: s };
54
+ }
55
+
56
+ // Bare wiki node token (starts with wik-prefix like wikcn/wikm/wikn and has body)
57
+ if (WIKI_BARE_RE.test(s)) {
58
+ return { kind: 'wiki', token: s, raw: s };
59
+ }
60
+
61
+ // Everything else is a raw native token — downstream will trust it as-is.
62
+ return { kind: 'raw', token: s, raw: s };
63
+ }
64
+
65
+ // --- LRU cache for wiki-node → obj resolution ---
66
+ // Wiki node tokens are long-lived; the obj_token and obj_type behind them
67
+ // essentially never change, so a 10-minute TTL prevents refetching the
68
+ // same node 20 times in one chain of tool calls but still lets the rare
69
+ // re-parented-node case catch up on its own.
70
+
71
+ const CACHE_MAX = 200;
72
+ const CACHE_TTL_MS = 10 * 60 * 1000;
73
+ const _cache = new Map(); // token → { value, expiresAt }
74
+
75
+ function _cacheGet(token) {
76
+ const e = _cache.get(token);
77
+ if (!e) return null;
78
+ if (Date.now() > e.expiresAt) {
79
+ _cache.delete(token);
80
+ return null;
81
+ }
82
+ // Refresh LRU position
83
+ _cache.delete(token);
84
+ _cache.set(token, e);
85
+ return e.value;
86
+ }
87
+
88
+ function _cacheSet(token, value) {
89
+ if (_cache.has(token)) _cache.delete(token);
90
+ _cache.set(token, { value, expiresAt: Date.now() + CACHE_TTL_MS });
91
+ // Evict oldest if over cap
92
+ while (_cache.size > CACHE_MAX) {
93
+ const firstKey = _cache.keys().next().value;
94
+ _cache.delete(firstKey);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Resolve a user-provided input into a concrete { obj_type, obj_token, space_id? }.
100
+ * @param {string} input
101
+ * @param {object} official LarkOfficialClient instance (for wiki getNode lookup)
102
+ * @returns {Promise<{obj_type: string, obj_token: string, space_id?: string, via: string}>}
103
+ * via: 'url-direct' | 'wiki-lookup' | 'raw-passthrough'
104
+ */
105
+ async function resolveToObj(input, official) {
106
+ const parsed = parseFeishuInput(input);
107
+
108
+ // Wiki node — must call getWikiNode to unwrap.
109
+ if (parsed.kind === 'wiki') {
110
+ const cached = _cacheGet(parsed.token);
111
+ if (cached) return { ...cached, via: 'wiki-lookup-cached' };
112
+ if (!official || typeof official.getWikiNode !== 'function') {
113
+ throw new Error('resolveToObj: wiki input requires an official client, none provided');
114
+ }
115
+ const node = await official.getWikiNode(parsed.token);
116
+ if (!node || !node.obj_token || !node.obj_type) {
117
+ throw new Error(`resolveToObj: wiki node ${parsed.token} missing obj_token/obj_type in response: ${JSON.stringify(node)}`);
118
+ }
119
+ const value = {
120
+ obj_type: node.obj_type, // e.g. 'docx' | 'sheet' | 'bitable' | 'mindnote' | 'slide' | 'file'
121
+ obj_token: node.obj_token,
122
+ space_id: node.space_id,
123
+ };
124
+ _cacheSet(parsed.token, value);
125
+ return { ...value, via: 'wiki-lookup' };
126
+ }
127
+
128
+ // Native URL with a direct doc/bitable/etc segment — just use the extracted token.
129
+ if (parsed.kind !== 'raw') {
130
+ return { obj_type: parsed.kind, obj_token: parsed.token, via: 'url-direct' };
131
+ }
132
+
133
+ // Raw — caller knows what they're doing, pass through without claiming a type.
134
+ return { obj_type: 'raw', obj_token: parsed.token, via: 'raw-passthrough' };
135
+ }
136
+
137
+ /**
138
+ * Convenience shortcut: resolve and return just the obj_token.
139
+ * Used in handler prologues where we only need the native token and trust the tool
140
+ * to know which surface it's hitting.
141
+ */
142
+ async function resolveToken(input, official) {
143
+ const r = await resolveToObj(input, official);
144
+ return r.obj_token;
145
+ }
146
+
147
+ module.exports = {
148
+ parseFeishuInput,
149
+ resolveToObj,
150
+ resolveToken,
151
+ };