@evandu/super-pi 0.1.152 → 0.1.153

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.
Files changed (34) hide show
  1. package/dist/client/.super-pi-client-sources.json +3 -3
  2. package/dist/client/assets/{CodeMirrorEditor-Cb6Drhk5.js → CodeMirrorEditor-DmRMgD_e.js} +2 -2
  3. package/dist/client/assets/{dist-BHWjjFy_.js → dist-1ehoscoU.js} +1 -1
  4. package/dist/client/assets/{dist-ByaeX0Xa.js → dist-BAy39a02.js} +1 -1
  5. package/dist/client/assets/{dist-BorepJPL.js → dist-BIMTBXpD.js} +1 -1
  6. package/dist/client/assets/{dist-Df1eD154.js → dist-BPU0RZX2.js} +1 -1
  7. package/dist/client/assets/{dist-BvW835kJ.js → dist-BQvKL2T4.js} +1 -1
  8. package/dist/client/assets/{dist-DnIfPAlr.js → dist-BTfxEdeF.js} +1 -1
  9. package/dist/client/assets/{dist-DgaBkcOR.js → dist-BWxApOy6.js} +1 -1
  10. package/dist/client/assets/{dist-CFBpvn_F.js → dist-BaagZpIV.js} +1 -1
  11. package/dist/client/assets/{dist-B8RyLyAM.js → dist-BjZSq94T.js} +1 -1
  12. package/dist/client/assets/{dist-Kd8Ts9Qe.js → dist-BzOPeLJM.js} +1 -1
  13. package/dist/client/assets/{dist-BqN0geUv.js → dist-C5OCcu2D.js} +1 -1
  14. package/dist/client/assets/{dist-dPJqK8GC.js → dist-CMNmeOX_.js} +1 -1
  15. package/dist/client/assets/{dist-CjbILrLW.js → dist-Cl_nos6D.js} +1 -1
  16. package/dist/client/assets/{dist-i6E_Fwl0.js → dist-DE8_FyRQ.js} +1 -1
  17. package/dist/client/assets/{dist-Cjk1RRIz.js → dist-DM15RCDF.js} +1 -1
  18. package/dist/client/assets/{dist-1P3rbXrP.js → dist-DpK_uu3H.js} +1 -1
  19. package/dist/client/assets/{dist-DkEKqzzI.js → dist-QCoWWp1z.js} +1 -1
  20. package/dist/client/assets/{dist-BgQmfor3.js → dist-SHt_aVda.js} +1 -1
  21. package/dist/client/assets/{dist-BzySVYC0.js → dist-YiNVn-GQ.js} +1 -1
  22. package/dist/client/assets/{dist-ijCQUPtv.js → dist-h0HvU8ex.js} +1 -1
  23. package/dist/client/assets/{dist-D7zfHJFz.js → dist-mY7dpaip.js} +1 -1
  24. package/dist/client/assets/{dist-CyFyjl9w.js → dist-tpn03yX2.js} +1 -1
  25. package/dist/client/assets/{index-CgRiEaG2.js → index-Ce896Hh9.js} +12 -12
  26. package/dist/client/index.html +1 -1
  27. package/dist/server/terminal.d.ts +29 -4
  28. package/dist/server/terminal.js +38 -5
  29. package/package.json +5 -3
  30. package/dist/.super-pi-check-inputs.json +0 -4
  31. package/dist/server/feishu-config.d.ts +0 -68
  32. package/dist/server/feishu-config.js +0 -84
  33. package/dist/server/feishu.d.ts +0 -187
  34. package/dist/server/feishu.js +0 -695
@@ -8,7 +8,7 @@
8
8
  <link rel="alternate icon" href="/favicon.ico" />
9
9
  <link rel="apple-touch-icon" href="/favicon.svg" />
10
10
  <title>Super Pi</title>
11
- <script type="module" crossorigin src="/assets/index-CgRiEaG2.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-Ce896Hh9.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-DCLaWQin.css">
13
13
  </head>
14
14
  <body>
@@ -29,8 +29,8 @@
29
29
  * nothing else; this file resolves the id against the store and spawns in that workspace's path.
30
30
  * A browser that asked for `C:\` or `/etc` gets its own workspace's directory instead, because
31
31
  * there is no code path on which a client-supplied path reaches `spawn`.
32
- * 4. BOUNDED SESSIONS. `MAX_SESSIONS` shells, one per client tab, capped so a loop of reconnects
33
- * cannot accumulate processes.
32
+ * 4. BOUNDED SESSIONS. `MAX_SESSIONS` live shells, capped so a loop of reconnects cannot accumulate
33
+ * processes.
34
34
  *
35
35
  * What is NOT here is a per-command allowlist. A terminal that refuses `rm` or `curl` is not a terminal,
36
36
  * and pretending otherwise would be worse than the honest version: with (1) and (2) the operator has
@@ -89,8 +89,33 @@ export declare function terminalShellCandidates(env?: NodeJS.ProcessEnv): {
89
89
  args: string[];
90
90
  label: string;
91
91
  }[];
92
- /** One shell per browser tab, and a hard ceiling so reconnects cannot accumulate processes. */
93
- export declare const MAX_SESSIONS = 8;
92
+ /**
93
+ * THE CEILING ON LIVE SHELLS.
94
+ *
95
+ * WHAT THIS COUNTS. Only the browser's terminal panel: `terminal.open` is the ONE place a shell is
96
+ * spawned, and it is a command the browser sends over its own WebSocket. An agent session, a
97
+ * sub-agent run, and the `exec` tool do NOT consume this budget - the exec tool spawns its own
98
+ * short-lived `shell -c` process through a different service.
99
+ *
100
+ * WHY THE COUNT ACCUMULATES. The client opens up to five terminals per session (`MAX_SLOTS` in
101
+ * TerminalPanel), and a shell is deliberately KEPT ALIVE across a session switch: the panel unmounts
102
+ * without closing it, so the work in that shell survives the switch. Every session in the tree is a
103
+ * conversation with its own panel - including a CHILD session the user can click into - and each of
104
+ * them can hold five shells, and all of them land in this one map. A tree multiplies: eight parents
105
+ * with four children each are forty sessions, and forty sessions at four terminals is a hundred and
106
+ * sixty shells.
107
+ *
108
+ * WHY 256. Every shell in this map was opened by the user on purpose, and what runs inside it is the
109
+ * user's own business - so this constant is NOT a memory governor and does not pretend to be one. Its
110
+ * whole job is to be generous enough never to stand in the way of real work, and loud enough that a
111
+ * client looping on `terminal.open` is stopped with a message instead of quietly filling the machine.
112
+ * (For the record, the platform's own overhead per shell is the shell: an idle Windows PowerShell is
113
+ * ~90MB and an idle bash ~8MB, so the cost of "many terminals" is the cost of the shells themselves.)
114
+ * Reconnect storms do not need this constant to be low: the socket-close cleanup already kills a
115
+ * connection's shells, and the 30-second heartbeat reaps the half-open connections that would
116
+ * otherwise leave them behind.
117
+ */
118
+ export declare const MAX_SESSIONS = 256;
94
119
  /**
95
120
  * The terminal's wire protocol, as members of the EXISTING `ClientCommand` / `ServerEvent` unions rather
96
121
  * than a second transport.
@@ -29,8 +29,8 @@
29
29
  * nothing else; this file resolves the id against the store and spawns in that workspace's path.
30
30
  * A browser that asked for `C:\` or `/etc` gets its own workspace's directory instead, because
31
31
  * there is no code path on which a client-supplied path reaches `spawn`.
32
- * 4. BOUNDED SESSIONS. `MAX_SESSIONS` shells, one per client tab, capped so a loop of reconnects
33
- * cannot accumulate processes.
32
+ * 4. BOUNDED SESSIONS. `MAX_SESSIONS` live shells, capped so a loop of reconnects cannot accumulate
33
+ * processes.
34
34
  *
35
35
  * What is NOT here is a per-command allowlist. A terminal that refuses `rm` or `curl` is not a terminal,
36
36
  * and pretending otherwise would be worse than the honest version: with (1) and (2) the operator has
@@ -190,8 +190,33 @@ export function terminalShellCandidates(env = process.env) {
190
190
  // ---------------------------------------------------------------------------------------------
191
191
  // Bounds
192
192
  // ---------------------------------------------------------------------------------------------
193
- /** One shell per browser tab, and a hard ceiling so reconnects cannot accumulate processes. */
194
- export const MAX_SESSIONS = 8;
193
+ /**
194
+ * THE CEILING ON LIVE SHELLS.
195
+ *
196
+ * WHAT THIS COUNTS. Only the browser's terminal panel: `terminal.open` is the ONE place a shell is
197
+ * spawned, and it is a command the browser sends over its own WebSocket. An agent session, a
198
+ * sub-agent run, and the `exec` tool do NOT consume this budget - the exec tool spawns its own
199
+ * short-lived `shell -c` process through a different service.
200
+ *
201
+ * WHY THE COUNT ACCUMULATES. The client opens up to five terminals per session (`MAX_SLOTS` in
202
+ * TerminalPanel), and a shell is deliberately KEPT ALIVE across a session switch: the panel unmounts
203
+ * without closing it, so the work in that shell survives the switch. Every session in the tree is a
204
+ * conversation with its own panel - including a CHILD session the user can click into - and each of
205
+ * them can hold five shells, and all of them land in this one map. A tree multiplies: eight parents
206
+ * with four children each are forty sessions, and forty sessions at four terminals is a hundred and
207
+ * sixty shells.
208
+ *
209
+ * WHY 256. Every shell in this map was opened by the user on purpose, and what runs inside it is the
210
+ * user's own business - so this constant is NOT a memory governor and does not pretend to be one. Its
211
+ * whole job is to be generous enough never to stand in the way of real work, and loud enough that a
212
+ * client looping on `terminal.open` is stopped with a message instead of quietly filling the machine.
213
+ * (For the record, the platform's own overhead per shell is the shell: an idle Windows PowerShell is
214
+ * ~90MB and an idle bash ~8MB, so the cost of "many terminals" is the cost of the shells themselves.)
215
+ * Reconnect storms do not need this constant to be low: the socket-close cleanup already kills a
216
+ * connection's shells, and the 30-second heartbeat reaps the half-open connections that would
217
+ * otherwise leave them behind.
218
+ */
219
+ export const MAX_SESSIONS = 256;
195
220
  /** Below the client panel's own minimum, and small enough that a `ls` cannot stall the socket. */
196
221
  const MIN_COLS = 20;
197
222
  const MIN_ROWS = 5;
@@ -296,7 +321,15 @@ export class TerminalService {
296
321
  if (this.sessions.has(command.sessionId))
297
322
  return;
298
323
  if (this.sessions.size >= MAX_SESSIONS) {
299
- send({ type: "terminal.error", sessionId: command.sessionId, message: `终端会话已达上限(${MAX_SESSIONS} 个),请先关闭一个。` });
324
+ // The count is in the message because the shells eating the budget are usually the BACKGROUND
325
+ // ones of other sessions - a parent session's children included, each of which has its own
326
+ // panel with its own terminals. The user sees three terminals in this panel and a refusal,
327
+ // and has no way to guess that the sessions they left open still hold shells.
328
+ send({
329
+ type: "terminal.error",
330
+ sessionId: command.sessionId,
331
+ message: `终端会话已达上限(服务器最多 ${MAX_SESSIONS} 个,当前已有 ${this.sessions.size} 个在运行)。切换会话后仍在后台运行的终端也计入其中(包括 session 树里的子会话),请到对应的会话里关闭一个再试。`,
332
+ });
300
333
  return;
301
334
  }
302
335
  // The workspace id is the ONLY thing the browser supplies, and it is resolved here. A client that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.152",
3
+ "version": "0.1.153",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  "node": ">=22.19.0"
30
30
  },
31
31
  "scripts": {
32
- "prepack": "npm run build",
33
- "prepublishOnly": "npm run build && cross-env SUPER_PI_REQUIRE_REAL_PI=1 node scripts/extension-load.test.mjs",
32
+ "prepack": "node scripts/clean-build.mjs",
33
+ "prepublishOnly": "node scripts/clean-build.mjs && cross-env SUPER_PI_REQUIRE_REAL_PI=1 node scripts/extension-load.test.mjs",
34
34
  "dev": "concurrently --kill-others-on-fail \"npm:dev:server\" \"npm:dev:client\"",
35
35
  "dev:client": "vite --host 0.0.0.0",
36
36
  "dev:server": "cross-env SUPER_PI_PORT=3081 tsx watch src/server/index.ts",
@@ -53,6 +53,7 @@
53
53
  "scripts/pi-session.test.mjs",
54
54
  "scripts/length-continuation.test.mjs",
55
55
  "scripts/pi-session-isolation.test.mjs",
56
+ "scripts/build-state.test.mjs",
56
57
  "scripts/installed-layout.test.mjs",
57
58
  "scripts/pi-model-switch.test.mjs",
58
59
  "scripts/pi-thinking-levels.test.mjs",
@@ -277,6 +278,7 @@
277
278
  "scripts/narrow-layout.test.mjs",
278
279
  "scripts/scrollbar-style.test.mjs",
279
280
  "scripts/stream-tail-browser.test.mjs",
281
+ "scripts/scroll-restore-browser.test.mjs",
280
282
  "scripts/theme-contrast.test.mjs",
281
283
  "scripts/theme-switch-state.test.mjs",
282
284
  "scripts/settings-window-structure.test.mjs",
@@ -1,4 +0,0 @@
1
- {
2
- "hash": "1219863c6dde78b6d52bf17deb4231e37ea1e686894543611a5c328017e6ac18",
3
- "recordedAt": "2026-11-01T17:52:28.907Z"
4
- }
@@ -1,68 +0,0 @@
1
- import type { FeishuDomain } from "./feishu.js";
2
- /**
3
- * THE FEISHU CREDENTIALS, and where they live.
4
- *
5
- * WHY A FILE IN THE DATA DIRECTORY AND NOT A FIELD IN THE APP STATE. The App State is broadcast to
6
- * every browser that connects, and it is written to disk in a form that is meant to be readable.
7
- * A secret does not belong there - the same rule the provider registry follows, where the API key
8
- * lives in `<dataDir>/providers.json` with restrictive permissions and the browser is told only
9
- * that a key exists.
10
- *
11
- * WHY THE ENVIRONMENT OVERRIDES THE FILE. A deployment that provisions the credential through the
12
- * environment should not have to write a file as well, and the environment is the harder thing to
13
- * override accidentally. The file is the fallback, not the other way round.
14
- *
15
- * WHY THERE IS ONE SET OF CREDENTIALS FOR THE WHOLE SERVER. Feishu credentials belong to an
16
- * application inside one enterprise, and Super Pi runs on one person's machine. A per-workspace
17
- * credential would be a second enterprise's application, and the user of that workspace would have
18
- * to be handed a secret they never asked for.
19
- */
20
- export type FeishuConfig = {
21
- /** The application's App ID. */
22
- appId?: string;
23
- /** The application's App Secret. Never leaves the server. */
24
- appSecret?: string;
25
- /** Which Feishu the calls go to. */
26
- domain?: FeishuDomain;
27
- };
28
- /** Where the credentials come from, for the panel to show. */
29
- export type FeishuConfigSource = "environment" | "file" | "none";
30
- export declare class FeishuConfigStore {
31
- private readonly file;
32
- constructor(file: string);
33
- /**
34
- * The credentials in force, and where they came from.
35
- *
36
- * The environment wins over the file, and the file is read only when the environment has not
37
- * said anything - so a machine that provisions the credential through the environment never
38
- * touches the file at all.
39
- */
40
- read(): Promise<{
41
- config: FeishuConfig;
42
- source: FeishuConfigSource;
43
- }>;
44
- /**
45
- * The file's own contents, and only its own contents.
46
- *
47
- * A file that cannot be read is a file that is not there, and a file that cannot be parsed is a
48
- * file this server did not write. Both are reported rather than thrown: a broken credential
49
- * file must not stop the server from starting.
50
- */
51
- private readFile;
52
- /**
53
- * Writes the credentials.
54
- *
55
- * The write is atomic (a temporary file and a rename) so a crash cannot leave half a JSON file
56
- * behind, and the file's mode is 0600 so that a secret is not readable by every account on the
57
- * machine. On Windows the mode is not enforced the same way, and there is nothing this code can
58
- * do about that.
59
- */
60
- write(config: FeishuConfig): Promise<void>;
61
- }
62
- /**
63
- * The domain a name means, defaulting to Feishu.
64
- *
65
- * A name this does not recognise is Feishu rather than an error: the domain is a convenience for
66
- * Lark users, and refusing to start over a typo in it would be worse than trying Feishu.
67
- */
68
- export declare function domainOf(value: string | undefined): FeishuDomain;
@@ -1,84 +0,0 @@
1
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { randomUUID } from "node:crypto";
3
- import { dirname } from "node:path";
4
- export class FeishuConfigStore {
5
- file;
6
- constructor(file) {
7
- this.file = file;
8
- }
9
- /**
10
- * The credentials in force, and where they came from.
11
- *
12
- * The environment wins over the file, and the file is read only when the environment has not
13
- * said anything - so a machine that provisions the credential through the environment never
14
- * touches the file at all.
15
- */
16
- async read() {
17
- const fromEnv = {
18
- ...(process.env.SUPER_PI_FEISHU_APP_ID ? { appId: process.env.SUPER_PI_FEISHU_APP_ID.trim() } : {}),
19
- ...(process.env.SUPER_PI_FEISHU_APP_SECRET ? { appSecret: process.env.SUPER_PI_FEISHU_APP_SECRET.trim() } : {}),
20
- ...(process.env.SUPER_PI_FEISHU_DOMAIN ? { domain: domainOf(process.env.SUPER_PI_FEISHU_DOMAIN) } : {}),
21
- };
22
- if (fromEnv.appId && fromEnv.appSecret)
23
- return { config: fromEnv, source: "environment" };
24
- const stored = await this.readFile();
25
- if (fromEnv.appId || fromEnv.appSecret)
26
- return { config: { ...stored, ...fromEnv }, source: "environment" };
27
- return { config: stored, source: stored.appId && stored.appSecret ? "file" : "none" };
28
- }
29
- /**
30
- * The file's own contents, and only its own contents.
31
- *
32
- * A file that cannot be read is a file that is not there, and a file that cannot be parsed is a
33
- * file this server did not write. Both are reported rather than thrown: a broken credential
34
- * file must not stop the server from starting.
35
- */
36
- async readFile() {
37
- try {
38
- const raw = await readFile(this.file, "utf8");
39
- const parsed = JSON.parse(raw);
40
- return {
41
- ...(typeof parsed.appId === "string" ? { appId: parsed.appId } : {}),
42
- ...(typeof parsed.appSecret === "string" ? { appSecret: parsed.appSecret } : {}),
43
- ...(parsed.domain ? { domain: domainOf(parsed.domain) } : {}),
44
- };
45
- }
46
- catch {
47
- return {};
48
- }
49
- }
50
- /**
51
- * Writes the credentials.
52
- *
53
- * The write is atomic (a temporary file and a rename) so a crash cannot leave half a JSON file
54
- * behind, and the file's mode is 0600 so that a secret is not readable by every account on the
55
- * machine. On Windows the mode is not enforced the same way, and there is nothing this code can
56
- * do about that.
57
- */
58
- async write(config) {
59
- await mkdir(dirname(this.file), { recursive: true });
60
- const temporary = `${this.file}.${randomUUID()}.tmp`;
61
- const value = {
62
- ...(config.appId ? { appId: config.appId } : {}),
63
- ...(config.appSecret ? { appSecret: config.appSecret } : {}),
64
- ...(config.domain ? { domain: config.domain } : {}),
65
- };
66
- try {
67
- await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
68
- await rename(temporary, this.file);
69
- }
70
- catch (error) {
71
- await writeFile(temporary, "{}", { mode: 0o600 }).catch(() => undefined);
72
- throw error;
73
- }
74
- }
75
- }
76
- /**
77
- * The domain a name means, defaulting to Feishu.
78
- *
79
- * A name this does not recognise is Feishu rather than an error: the domain is a convenience for
80
- * Lark users, and refusing to start over a typo in it would be worse than trying Feishu.
81
- */
82
- export function domainOf(value) {
83
- return String(value ?? "").trim().toLowerCase() === "lark" ? "lark" : "feishu";
84
- }
@@ -1,187 +0,0 @@
1
- /**
2
- * THE FEISHU CLIENT: everything Super Pi knows how to do with a cloud document on Feishu.
3
- *
4
- * WHY THIS LAYER EXISTS AT ALL. Feishu's document API is not "read a file" and "write a file". A
5
- * document is a TREE OF BLOCKS, the API refuses to create a document with content in it, and the
6
- * official way to put Markdown into a document is a three-step dance (convert to blocks, then
7
- * insert them as nested blocks under the page block). An agent that had to know that would be
8
- * writing protocol details into every prompt. So the protocol lives here, once, and what is left
9
- * above it is "read this document" and "write this document".
10
- *
11
- * WHY AN HTTP CLIENT AND NOT THE OFFICIAL SDK. The official SDK (`@larksuiteoapi/node-sdk`) is a
12
- * good library, but it is a dependency of the PACKAGE, and this package's whole point is that it
13
- * installs and runs. Every call it makes is a plain HTTP call with a bearer token, so the four
14
- * endpoints this feature needs are four functions - and the credential handling (a token that
15
- * expires, a token that has to be refreshed, a rate limit that has to be waited out) is the part
16
- * that is worth writing carefully and testing on its own.
17
- *
18
- * WHAT THE IDENTITY IS. Everything here runs as the APPLICATION (`tenant_access_token`), never as
19
- * a person. That is the identity that needs no OAuth dance and no browser, and it is also the
20
- * identity that can only touch the documents the application has been made a collaborator on.
21
- * See `docs/FEISHU.md` for the setup that implies, because it is the one thing a user must do
22
- * before any of this works.
23
- */
24
- /** The tool that reads a Feishu document. */
25
- export declare const FEISHU_READ_TOOL_NAME = "feishu_doc_read";
26
- /** The tool that writes a Feishu document. */
27
- export declare const FEISHU_WRITE_TOOL_NAME = "feishu_doc_write";
28
- /** Which flavour of Feishu we talk to. The API paths differ, the shapes do not. */
29
- export type FeishuDomain = "feishu" | "lark";
30
- /** A block in a Feishu document. Only the fields this code reads are named. */
31
- export type FeishuBlock = {
32
- block_id: string;
33
- parent_id?: string;
34
- children?: string[];
35
- block_type: number;
36
- [key: string]: unknown;
37
- };
38
- /**
39
- * A REFUSAL FROM FEISHU, with the code kept alongside the message.
40
- *
41
- * The code is kept because the message Feishu sends is written for a developer reading a log,
42
- * and the part a person needs - "add this application as a collaborator", "open this permission
43
- * point" - is not in it.
44
- */
45
- export declare class FeishuError extends Error {
46
- readonly code: number;
47
- readonly hint?: string | undefined;
48
- constructor(code: number, message: string, hint?: string | undefined);
49
- }
50
- /**
51
- * The message a refusal is shown with: what Feishu said, and what to do about it.
52
- */
53
- export declare function describeFeishuError(code: number, message: string): string;
54
- /**
55
- * WHAT A URL OR A TOKEN NAMES.
56
- *
57
- * `kind` is "docx" for a document and "wiki" for a knowledge-base node. The distinction is not
58
- * cosmetic: a wiki URL's token is NOT the document id, and passing it as one produces a document
59
- * that does not exist. A wiki link has to be resolved through the wiki API first.
60
- */
61
- export type FeishuTarget = {
62
- kind: "docx" | "wiki";
63
- token: string;
64
- /** The tenant host the link came from, when it came from a link. Used to build a URL for a new document. */
65
- host?: string;
66
- };
67
- /**
68
- * Reads a document reference: a full URL, or a bare token.
69
- *
70
- * WHY BARE TOKENS ARE ACCEPTED. A person reading a document's URL sees the token in it, and an
71
- * agent that has been handed `doxcn...` should not have to be told to wrap it in a URL first.
72
- */
73
- export declare function parseFeishuTarget(input: string): FeishuTarget;
74
- /**
75
- * Renders a document's blocks as Markdown.
76
- *
77
- * WHY MARKDOWN AND NOT THE RAW TEXT. `raw_content` gives the words without the shape: a heading
78
- * arrives as a heading's text, a list arrives as separate lines, and a reader cannot tell them
79
- * apart. The block tree is the document, and Markdown is the closest plain-text shape this side of
80
- * the product already understands.
81
- */
82
- export declare function blocksToMarkdown(blocks: FeishuBlock[]): string;
83
- /** The client the service drives. Every call goes through here. */
84
- export type FeishuClientOptions = {
85
- appId: string;
86
- appSecret: string;
87
- domain?: FeishuDomain;
88
- /** Injected in tests, so the suite never touches the network. */
89
- fetchImpl?: typeof fetch;
90
- };
91
- export declare class FeishuClient {
92
- private readonly options;
93
- private token?;
94
- /** Whether the newer OAuth token endpoint works for this application. */
95
- private useLegacyAuth;
96
- /** The write queue. One write at a time, spaced by the interval the API allows. */
97
- private writeChain;
98
- private lastWriteAt;
99
- constructor(options: FeishuClientOptions);
100
- private get apiBase();
101
- private get accountsBase();
102
- private get fetchImpl();
103
- /**
104
- * THE ACCESS TOKEN, fetched once and reused.
105
- *
106
- * The lifetime is two hours and the API says to believe the number it returns rather than a
107
- * hardcoded one, so the expiry comes from the response and the token is renewed a couple of
108
- * minutes before it runs out.
109
- */
110
- accessToken(force?: boolean): Promise<string>;
111
- /**
112
- * The current OAuth endpoint: `POST https://accounts.feishu.cn/oauth/v3/token`.
113
- *
114
- * It is the one the documentation recommends. If it refuses - an older application, a tenant
115
- * that has not been migrated - the legacy endpoint is tried instead, and the client remembers
116
- * so the failing call is not made again on every token refresh.
117
- */
118
- private fetchOAuthToken;
119
- /**
120
- * The historical endpoint: `POST /open-apis/auth/v3/tenant_access_token/internal`.
121
- *
122
- * It is documented as superseded, but it is the endpoint every 自建应用 has worked with for
123
- * years, and a fallback that costs one request is cheaper than a feature that does not run.
124
- */
125
- private fetchLegacyToken;
126
- /**
127
- * ONE CALL, with the bearer token attached.
128
- *
129
- * A 429 is waited out rather than reported: the API tells us how many seconds until the window
130
- * resets, and a write that gives up because it was 200ms early is a write that did not happen.
131
- */
132
- private request;
133
- /**
134
- * Runs one write, spaced from the last one.
135
- */
136
- private withWriteSlot;
137
- /** The basic information about a document: its title and its version. */
138
- documentMeta(documentId: string): Promise<{
139
- documentId: string;
140
- title?: string;
141
- revisionId?: number;
142
- }>;
143
- /** Every block of a document, across every page. */
144
- documentBlocks(documentId: string): Promise<FeishuBlock[]>;
145
- /** The plain text of a document, without any structure. */
146
- rawContent(documentId: string): Promise<string>;
147
- /** The blocks directly under the page block - the document's top level. */
148
- rootChildren(documentId: string): Promise<string[]>;
149
- /** Creates an empty document. The content goes in afterwards, because the API has no other way. */
150
- createDocument(title: string, folderToken?: string): Promise<{
151
- documentId: string;
152
- revisionId?: number;
153
- }>;
154
- /** Resolves a knowledge-base node to the document behind it. */
155
- wikiNode(token: string): Promise<{
156
- objToken: string;
157
- objType: string;
158
- title?: string;
159
- }>;
160
- /**
161
- * Turns Markdown into the blocks that represent it.
162
- *
163
- * The API will not create a document with content in it, and building the blocks by hand would
164
- * mean reimplementing the conversion Feishu already does - tables, code fences, todos and all.
165
- */
166
- convertMarkdown(markdown: string): Promise<{
167
- blocks: FeishuBlock[];
168
- firstLevelBlockIds: string[];
169
- }>;
170
- /**
171
- * Inserts blocks under the page block.
172
- *
173
- * THE CHUNKING IS NOT AN OPTIMISATION. The API accepts at most 1000 blocks per call, and the
174
- * blocks of one subtree cannot be split across calls - so the top-level blocks are packed into
175
- * batches that each fit, and a document of any length goes in.
176
- */
177
- insertBlocks(documentId: string, blocks: FeishuBlock[], firstLevelBlockIds: string[], index?: number): Promise<number | undefined>;
178
- /** Deletes a range of the page block's children. The range is left-closed and right-open. */
179
- deleteChildren(documentId: string, startIndex: number, endIndex: number): Promise<number | undefined>;
180
- }
181
- /**
182
- * Removes the read-only merge information from a table block.
183
- *
184
- * The API rejects a table block that carries `merge_info`, and the conversion endpoint hands it
185
- * back with that field on it. The documentation says to strip it, so it is stripped.
186
- */
187
- export declare function stripTableMergeInfo(block: FeishuBlock): FeishuBlock;