@evandu/super-pi 0.1.151 → 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.
- package/dist/client/.super-pi-client-sources.json +6 -4
- package/dist/client/assets/{CodeMirrorEditor-BhTJvXpU.js → CodeMirrorEditor-DmRMgD_e.js} +2 -2
- package/dist/client/assets/{dist-BOqRI6sA.js → dist-1ehoscoU.js} +1 -1
- package/dist/client/assets/{dist-BFAjSsad.js → dist-BAy39a02.js} +1 -1
- package/dist/client/assets/{dist-80xX2Yd8.js → dist-BIMTBXpD.js} +1 -1
- package/dist/client/assets/{dist-TwlTH8F1.js → dist-BPU0RZX2.js} +1 -1
- package/dist/client/assets/{dist-49TFFUQg.js → dist-BQvKL2T4.js} +1 -1
- package/dist/client/assets/{dist-B0gI-KRY.js → dist-BTfxEdeF.js} +1 -1
- package/dist/client/assets/{dist-Dq7hLZoB.js → dist-BWxApOy6.js} +1 -1
- package/dist/client/assets/{dist-DDdxjRBp.js → dist-BaagZpIV.js} +1 -1
- package/dist/client/assets/{dist-Bz1OET8s.js → dist-BjZSq94T.js} +1 -1
- package/dist/client/assets/{dist-DJdFGvR0.js → dist-BzOPeLJM.js} +1 -1
- package/dist/client/assets/{dist-0vbzT0fs.js → dist-C5OCcu2D.js} +1 -1
- package/dist/client/assets/{dist-CMiCGuFr.js → dist-CMNmeOX_.js} +1 -1
- package/dist/client/assets/{dist-CzCFepaD.js → dist-Cl_nos6D.js} +1 -1
- package/dist/client/assets/{dist-aOpTOMHI.js → dist-DE8_FyRQ.js} +1 -1
- package/dist/client/assets/{dist-BuDFattZ.js → dist-DM15RCDF.js} +1 -1
- package/dist/client/assets/{dist-BstoGC94.js → dist-DpK_uu3H.js} +1 -1
- package/dist/client/assets/{dist-SMKcq1ns.js → dist-QCoWWp1z.js} +1 -1
- package/dist/client/assets/{dist-CkatEAPx.js → dist-SHt_aVda.js} +1 -1
- package/dist/client/assets/{dist-B3Dy7iF_.js → dist-YiNVn-GQ.js} +1 -1
- package/dist/client/assets/{dist-B5nvkwVU.js → dist-h0HvU8ex.js} +1 -1
- package/dist/client/assets/{dist-Cwuref9o.js → dist-mY7dpaip.js} +1 -1
- package/dist/client/assets/{dist-Ce7WvAkm.js → dist-tpn03yX2.js} +1 -1
- package/dist/client/assets/{index-Csr3-JTY.js → index-Ce896Hh9.js} +16 -16
- package/dist/client/assets/index-DCLaWQin.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/terminal.d.ts +29 -4
- package/dist/server/terminal.js +38 -5
- package/package.json +6 -3
- package/dist/.super-pi-check-inputs.json +0 -4
- package/dist/client/assets/index-BMpqviwU.css +0 -1
- package/dist/server/feishu-config.d.ts +0 -68
- package/dist/server/feishu-config.js +0 -84
- package/dist/server/feishu.d.ts +0 -187
- package/dist/server/feishu.js +0 -695
|
@@ -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
|
-
}
|
package/dist/server/feishu.d.ts
DELETED
|
@@ -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;
|