@evandu/super-pi 0.1.131 → 0.1.133
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/README.md +18 -0
- package/agents/super-pi-feishu/index.ts +88 -0
- package/agents/super-pi-knowledge/index.ts +1 -1
- package/agents/super-pi-task/core.ts +177 -0
- package/agents/super-pi-task/index.ts +59 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +12 -9
- package/dist/client/assets/CodeMirrorEditor-CZZfGD0U.js +2 -0
- package/dist/client/assets/{dist-B448EWTM.js → dist-0FH-oFco.js} +1 -1
- package/dist/client/assets/{dist-Fhkmvjdx.js → dist-4Pts9xPO.js} +1 -1
- package/dist/client/assets/{dist-BcE6tJvc.js → dist-B5ruVtUG.js} +1 -1
- package/dist/client/assets/{dist-CpuexUlh.js → dist-BJinarhP.js} +1 -1
- package/dist/client/assets/{dist-Cybsoapc.js → dist-BffE00vI.js} +1 -1
- package/dist/client/assets/{dist-DxoWS5MH.js → dist-BtTFCNVQ.js} +1 -1
- package/dist/client/assets/{dist-CtdlYgGc.js → dist-C2nuBCP4.js} +1 -1
- package/dist/client/assets/{dist-hfpd-aWa.js → dist-CApAttfj.js} +1 -1
- package/dist/client/assets/{dist-D6PoSxR5.js → dist-CE5BnwJa.js} +1 -1
- package/dist/client/assets/{dist-COp25nhe.js → dist-CFPXj4ws.js} +1 -1
- package/dist/client/assets/{dist-D7bj81KQ.js → dist-CUKaDTyO.js} +1 -1
- package/dist/client/assets/{dist-BFlq-kiF.js → dist-CYD2SqmG.js} +1 -1
- package/dist/client/assets/{dist-lQWWWxh9.js → dist-CxsunljU.js} +1 -1
- package/dist/client/assets/{dist-BXVFgriA.js → dist-D4x1XTQb.js} +1 -1
- package/dist/client/assets/{dist-DXNDtyHP.js → dist-DHqqpxo4.js} +1 -1
- package/dist/client/assets/{dist-r5Af94tH.js → dist-DOYWHMIq.js} +1 -1
- package/dist/client/assets/{dist-B68Q8uXX.js → dist-DQf5nNxa.js} +1 -1
- package/dist/client/assets/{dist-C6qSkkeN.js → dist-DbbBgSwL.js} +1 -1
- package/dist/client/assets/{dist-B1_1Ui9E.js → dist-LFfokYJs.js} +1 -1
- package/dist/client/assets/{dist-ByIqsb5-.js → dist-aL2NGji_.js} +1 -1
- package/dist/client/assets/{dist-C9AkgNbt.js → dist-azDN8a65.js} +1 -1
- package/dist/client/assets/{dist-DP2ygAFF.js → dist-fYp8Xwrx.js} +1 -1
- package/dist/client/assets/{index-CXB6U5mz.css → index-D3MEG8uH.css} +1 -1
- package/dist/client/assets/index-YEN4ysHF.js +100 -0
- package/dist/client/index.html +2 -2
- package/dist/server/agents.js +21 -1
- package/dist/server/application/feishu-service.d.ts +179 -0
- package/dist/server/application/feishu-service.js +358 -0
- package/dist/server/application/run-service.js +15 -1
- package/dist/server/application/scheduled-task-service.d.ts +164 -0
- package/dist/server/application/scheduled-task-service.js +350 -0
- package/dist/server/command-handler.d.ts +25 -1
- package/dist/server/command-handler.js +60 -1
- package/dist/server/feishu-cli.d.ts +223 -0
- package/dist/server/feishu-cli.js +453 -0
- package/dist/server/feishu-config.d.ts +68 -0
- package/dist/server/feishu-config.js +84 -0
- package/dist/server/feishu.d.ts +187 -0
- package/dist/server/feishu.js +695 -0
- package/dist/server/files.js +3 -3
- package/dist/server/http-server.js +232 -1
- package/dist/server/index.js +29 -2
- package/dist/server/knowledge.js +10 -2
- package/dist/server/persistence/app-state-repository.d.ts +13 -1
- package/dist/server/persistence/sqlite-state-repository.js +21 -0
- package/dist/server/pi-session-events.js +10 -2
- package/dist/server/pi-session-pool.d.ts +33 -1
- package/dist/server/pi-session-pool.js +20 -2
- package/dist/server/pi-session.js +3 -0
- package/dist/server/runtime.d.ts +47 -1
- package/dist/server/runtime.js +110 -15
- package/dist/server/scheduled-tasks.d.ts +101 -0
- package/dist/server/scheduled-tasks.js +138 -0
- package/dist/server/server-context.d.ts +16 -0
- package/dist/server/store.d.ts +30 -0
- package/dist/server/store.js +81 -2
- package/dist/server/task-tool.d.ts +18 -0
- package/dist/server/task-tool.js +18 -0
- package/dist/server/types.d.ts +56 -0
- package/package.json +26 -4
- package/dist/client/assets/CodeMirrorEditor-BlQhH3vs.js +0 -2
- package/dist/client/assets/index-BApJ3Zbq.js +0 -98
package/dist/client/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
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-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-YEN4ysHF.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D3MEG8uH.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/dist/server/agents.js
CHANGED
|
@@ -447,7 +447,27 @@ export async function readAgents(directory, options = {}) {
|
|
|
447
447
|
throw error;
|
|
448
448
|
}
|
|
449
449
|
for (const file of entries.filter((name) => name.toLowerCase().endsWith(".md")).sort()) {
|
|
450
|
-
|
|
450
|
+
// A FILE THAT CANNOT BE READ IS THE SAME KIND OF PROBLEM AS ONE THAT CANNOT BE PARSED.
|
|
451
|
+
//
|
|
452
|
+
// The function's own reason for existing says it plainly: "one typo in one agent must not take
|
|
453
|
+
// down the picker and hide every other agent with it". A file that cannot be READ is at least as
|
|
454
|
+
// ordinary as that - on Windows "another program has this file open" is a Tuesday, not an
|
|
455
|
+
// emergency - and it used to be the one unguarded step in this loop: the parse failure below is
|
|
456
|
+
// collected as a problem, while the read failure was thrown, so a single locked file emptied the
|
|
457
|
+
// whole directory. Four callers ate that: the main agent's own definition (the turn could not
|
|
458
|
+
// start), the roster handed to sub-agents on every turn, the roster used when dispatching one,
|
|
459
|
+
// and the picker (which came back with an empty agent list).
|
|
460
|
+
let source;
|
|
461
|
+
try {
|
|
462
|
+
source = await readFile(join(dir, file), "utf8");
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
const fileId = file.replace(/\.md$/i, "");
|
|
466
|
+
if (!seenIds.has(fileId)) {
|
|
467
|
+
problems.push({ file, message: `读取失败:${error instanceof Error ? error.message : String(error)}` });
|
|
468
|
+
}
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
451
471
|
const parsed = parseDefinition(file, source);
|
|
452
472
|
if ("message" in parsed) {
|
|
453
473
|
const fileId = file.replace(/\.md$/i, "");
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { FeishuCli } from "../feishu-cli.js";
|
|
2
|
+
/**
|
|
3
|
+
* The largest request the Feishu endpoint accepts.
|
|
4
|
+
*
|
|
5
|
+
* The Markdown of a document rides in the request body, so the cap is generous for a document and
|
|
6
|
+
* small enough that a runaway paste is refused rather than accepted.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_FEISHU_REQUEST_BYTES: number;
|
|
9
|
+
/**
|
|
10
|
+
* WHAT THE PANEL NEEDS TO SHOW, in words a person can act on.
|
|
11
|
+
*/
|
|
12
|
+
export type FeishuStatus = {
|
|
13
|
+
/** Whether the official CLI is on this machine at all. */
|
|
14
|
+
installed: boolean;
|
|
15
|
+
version?: string;
|
|
16
|
+
/** The application the CLI is configured with, when it has one. */
|
|
17
|
+
appId?: string;
|
|
18
|
+
/** Whether a person has authorized the CLI to act as them. */
|
|
19
|
+
userReady: boolean;
|
|
20
|
+
/** Whether the CLI can act as the application itself. */
|
|
21
|
+
botReady: boolean;
|
|
22
|
+
/** The identity the CLI will use by default. */
|
|
23
|
+
identity?: "user" | "bot";
|
|
24
|
+
/** Whether the agent can read and write documents right now. */
|
|
25
|
+
connected: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* WHICH STEP OF THE GUIDE THE PERSON IS ON.
|
|
28
|
+
*
|
|
29
|
+
* 1 = install the CLI, 2 = connect the account, 3 = ready. The panel draws the three steps and
|
|
30
|
+
* marks this one, so "what do I do now" is answered by where the cursor is rather than by a
|
|
31
|
+
* sentence the person has to interpret.
|
|
32
|
+
*/
|
|
33
|
+
step: 1 | 2 | 3;
|
|
34
|
+
/** What to do next, in one sentence. */
|
|
35
|
+
message: string;
|
|
36
|
+
/** The CLI's own words, when it had something to say about the situation. */
|
|
37
|
+
detail?: string;
|
|
38
|
+
/** The CLI's own hint, which usually names the exact command to run. */
|
|
39
|
+
hint?: string;
|
|
40
|
+
/** Whether the check actually reached Feishu. */
|
|
41
|
+
verified?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* WHETHER THE APPLICATION ITSELF HAS TO BE REBUILT.
|
|
44
|
+
*
|
|
45
|
+
* Distinct from "step 2": a person whose application is fine but who has not logged in still
|
|
46
|
+
* needs step 2, while a person whose credential was refused needs a NEW application. Telling
|
|
47
|
+
* those two apart is what keeps a transient network failure from being answered by "let me
|
|
48
|
+
* create you another application".
|
|
49
|
+
*/
|
|
50
|
+
rebuild?: boolean;
|
|
51
|
+
};
|
|
52
|
+
/** The outcome of starting the connect flow. */
|
|
53
|
+
export type FeishuConnectResult = {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
/** Which step the person is on: creating an application, or authorizing their account. */
|
|
56
|
+
step?: "app" | "login";
|
|
57
|
+
/** The URL to open in a browser. */
|
|
58
|
+
verificationUri?: string;
|
|
59
|
+
/** The device code, kept server-side until the person says they are done. */
|
|
60
|
+
deviceCode?: string;
|
|
61
|
+
message: string;
|
|
62
|
+
/** Feishu's own "what to do about it", when there was one. */
|
|
63
|
+
hint?: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* THE FEISHU SERVICE: what an agent can do with the cloud documents of a Feishu enterprise.
|
|
67
|
+
*
|
|
68
|
+
* IT OWNS NO CREDENTIAL. The official CLI owns them, in the OS keychain, and this service only
|
|
69
|
+
* runs commands. That is the whole point of going through the CLI: Super Pi never sees an App
|
|
70
|
+
* Secret, never sees a user token, and never writes a credential file to disk.
|
|
71
|
+
*/
|
|
72
|
+
export declare class FeishuService {
|
|
73
|
+
private readonly cli;
|
|
74
|
+
/** The device code of a login that is waiting for the person to authorize it. */
|
|
75
|
+
private pendingDeviceCode?;
|
|
76
|
+
/** Whether an application registration is running and waiting for the browser confirmation. */
|
|
77
|
+
private pendingAppCreation;
|
|
78
|
+
constructor(cli?: FeishuCli);
|
|
79
|
+
/**
|
|
80
|
+
* WHAT THE CLI CAN DO RIGHT NOW.
|
|
81
|
+
*
|
|
82
|
+
* The states a person can be in, and what each one means for the agent:
|
|
83
|
+
*
|
|
84
|
+
* - The CLI is not installed: nothing works, and the fix is one command.
|
|
85
|
+
* - The CLI has no application: `config init --new` creates one through a browser confirmation,
|
|
86
|
+
* so the developer console is never visited.
|
|
87
|
+
* - The application exists but its credential is no longer accepted: the CLI says
|
|
88
|
+
* `verify_failed` and names the reason, and the fix is a new application - which is the same
|
|
89
|
+
* browser confirmation, so it stays one button.
|
|
90
|
+
* - The application works but no person is logged in: the agent can act as the application, but
|
|
91
|
+
* it cannot reach that person's own documents until they authorize it.
|
|
92
|
+
*/
|
|
93
|
+
status(): Promise<FeishuStatus>;
|
|
94
|
+
/**
|
|
95
|
+
* CHECKS THE CONNECTION, and says whether it actually works.
|
|
96
|
+
*
|
|
97
|
+
* The status already verifies; this is the same answer shaped for a button, so that a person can
|
|
98
|
+
* ask "it still doesn't work, why?" and get the CLI's own verdict rather than a guess.
|
|
99
|
+
*/
|
|
100
|
+
verify(): Promise<{
|
|
101
|
+
ok: boolean;
|
|
102
|
+
message: string;
|
|
103
|
+
detail?: string;
|
|
104
|
+
hint?: string;
|
|
105
|
+
}>;
|
|
106
|
+
/**
|
|
107
|
+
* STARTS THE CONNECT FLOW.
|
|
108
|
+
*
|
|
109
|
+
* Two steps, and which one runs is decided by what the CLI already has:
|
|
110
|
+
*
|
|
111
|
+
* - No application, or one whose credential no longer works: `config init --new` registers a
|
|
112
|
+
* fresh application through a browser confirmation. It BLOCKS until the person confirms, so it
|
|
113
|
+
* runs in the background and the URL is taken from its output. The broken case goes here too,
|
|
114
|
+
* because a new application is the one repair that needs no secret to be typed into a form.
|
|
115
|
+
* - A working application but no person: `auth login --no-wait --json` asks for a device code
|
|
116
|
+
* and hands back the URL to open. The code is kept here until the person says they are done.
|
|
117
|
+
*/
|
|
118
|
+
connect(): Promise<FeishuConnectResult>;
|
|
119
|
+
/**
|
|
120
|
+
* FINISHES THE FLOW, once the person says they are done.
|
|
121
|
+
*
|
|
122
|
+
* TWO KINDS OF "DONE", and the difference is what the CLI is waiting for:
|
|
123
|
+
*
|
|
124
|
+
* - An application is being registered: `config init --new` finishes by itself once the person
|
|
125
|
+
* confirms in the browser, so there is nothing to hand back. The status is re-read and the
|
|
126
|
+
* answer is whether the application showed up.
|
|
127
|
+
* - A login is being authorized: the device code is handed back to the CLI, which completes the
|
|
128
|
+
* exchange and stores the token in the keychain. Super Pi never sees the token itself.
|
|
129
|
+
*/
|
|
130
|
+
completeConnect(): Promise<FeishuConnectResult>;
|
|
131
|
+
/**
|
|
132
|
+
* READS A DOCUMENT.
|
|
133
|
+
*
|
|
134
|
+
* The CLI resolves a knowledge-base link itself, so a `/wiki/` URL is passed through untouched -
|
|
135
|
+
* and it answers with Markdown, which is the shape a model wants.
|
|
136
|
+
*/
|
|
137
|
+
read(input: {
|
|
138
|
+
url: string;
|
|
139
|
+
}): Promise<{
|
|
140
|
+
documentId?: string;
|
|
141
|
+
revisionId?: number;
|
|
142
|
+
title?: string;
|
|
143
|
+
url: string;
|
|
144
|
+
format: "markdown";
|
|
145
|
+
content: string;
|
|
146
|
+
}>;
|
|
147
|
+
/**
|
|
148
|
+
* WRITES A DOCUMENT.
|
|
149
|
+
*
|
|
150
|
+
* THREE MODES, and the difference between them is who decides what survives:
|
|
151
|
+
*
|
|
152
|
+
* - `create` makes a new document. Nothing can be lost, because nothing was there.
|
|
153
|
+
* - `append` adds to the end. Nothing can be lost either.
|
|
154
|
+
* - `replace` replaces the whole document. This one CAN lose a person's work, so it needs the
|
|
155
|
+
* revision the caller read - the same rule the file panel's write lives by, and for the same
|
|
156
|
+
* reason: an agent told to "把这份报告改成正式版" would otherwise destroy a hand edit made an
|
|
157
|
+
* hour ago, with no refusal and no warning.
|
|
158
|
+
*
|
|
159
|
+
* THE REVISION IS THE ETAG. The CLI passes it as the base revision of the edit, and Feishu
|
|
160
|
+
* refuses the edit when the document has moved on - so the check is enforced by the API rather
|
|
161
|
+
* than by a comparison Super Pi makes and then has to keep honest.
|
|
162
|
+
*/
|
|
163
|
+
write(input: {
|
|
164
|
+
url?: string;
|
|
165
|
+
content: string;
|
|
166
|
+
mode?: "create" | "append" | "replace";
|
|
167
|
+
title?: string;
|
|
168
|
+
baseRevisionId?: string;
|
|
169
|
+
}): Promise<{
|
|
170
|
+
documentId?: string;
|
|
171
|
+
revisionId?: number;
|
|
172
|
+
url?: string;
|
|
173
|
+
mode: "create" | "append" | "replace";
|
|
174
|
+
}>;
|
|
175
|
+
/**
|
|
176
|
+
* THE TOOL CALL, as the extension sends it.
|
|
177
|
+
*/
|
|
178
|
+
tool(body: unknown): Promise<unknown>;
|
|
179
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { FeishuCli, FeishuCliError, errorHint, errorMessage } from "../feishu-cli.js";
|
|
2
|
+
/**
|
|
3
|
+
* The largest request the Feishu endpoint accepts.
|
|
4
|
+
*
|
|
5
|
+
* The Markdown of a document rides in the request body, so the cap is generous for a document and
|
|
6
|
+
* small enough that a runaway paste is refused rather than accepted.
|
|
7
|
+
*/
|
|
8
|
+
export const MAX_FEISHU_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
9
|
+
/**
|
|
10
|
+
* THE FEISHU SERVICE: what an agent can do with the cloud documents of a Feishu enterprise.
|
|
11
|
+
*
|
|
12
|
+
* IT OWNS NO CREDENTIAL. The official CLI owns them, in the OS keychain, and this service only
|
|
13
|
+
* runs commands. That is the whole point of going through the CLI: Super Pi never sees an App
|
|
14
|
+
* Secret, never sees a user token, and never writes a credential file to disk.
|
|
15
|
+
*/
|
|
16
|
+
export class FeishuService {
|
|
17
|
+
cli;
|
|
18
|
+
/** The device code of a login that is waiting for the person to authorize it. */
|
|
19
|
+
pendingDeviceCode;
|
|
20
|
+
/** Whether an application registration is running and waiting for the browser confirmation. */
|
|
21
|
+
pendingAppCreation = false;
|
|
22
|
+
constructor(cli = new FeishuCli()) {
|
|
23
|
+
this.cli = cli;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* WHAT THE CLI CAN DO RIGHT NOW.
|
|
27
|
+
*
|
|
28
|
+
* The states a person can be in, and what each one means for the agent:
|
|
29
|
+
*
|
|
30
|
+
* - The CLI is not installed: nothing works, and the fix is one command.
|
|
31
|
+
* - The CLI has no application: `config init --new` creates one through a browser confirmation,
|
|
32
|
+
* so the developer console is never visited.
|
|
33
|
+
* - The application exists but its credential is no longer accepted: the CLI says
|
|
34
|
+
* `verify_failed` and names the reason, and the fix is a new application - which is the same
|
|
35
|
+
* browser confirmation, so it stays one button.
|
|
36
|
+
* - The application works but no person is logged in: the agent can act as the application, but
|
|
37
|
+
* it cannot reach that person's own documents until they authorize it.
|
|
38
|
+
*/
|
|
39
|
+
async status() {
|
|
40
|
+
const status = await this.cli.status();
|
|
41
|
+
if (!status.installed) {
|
|
42
|
+
return {
|
|
43
|
+
installed: false,
|
|
44
|
+
userReady: false,
|
|
45
|
+
botReady: false,
|
|
46
|
+
connected: false,
|
|
47
|
+
step: 1,
|
|
48
|
+
message: "还没有安装官方飞书 CLI。在终端里跑一次 npm i -g @larksuite/cli 就行,装完回到这里点「连接飞书账号」。",
|
|
49
|
+
...(status.hint ? { hint: status.hint } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const common = {
|
|
53
|
+
installed: true,
|
|
54
|
+
...(status.version ? { version: status.version } : {}),
|
|
55
|
+
...(status.appId ? { appId: status.appId } : {}),
|
|
56
|
+
...(status.verified !== undefined ? { verified: status.verified } : {}),
|
|
57
|
+
...(status.note ? { detail: status.note } : {}),
|
|
58
|
+
};
|
|
59
|
+
if (!status.appId) {
|
|
60
|
+
return {
|
|
61
|
+
...common,
|
|
62
|
+
userReady: false,
|
|
63
|
+
botReady: false,
|
|
64
|
+
connected: false,
|
|
65
|
+
step: 2,
|
|
66
|
+
message: "CLI 已就绪,还差一个应用。点「连接飞书账号」,浏览器里确认一下就有了——不用去开发者后台注册。",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// A CREDENTIAL THE CLI KNOWS IS BROKEN is the case a plain status would have hidden: the local
|
|
70
|
+
// file still holds an app, so the panel would have said "已配置", and every document call
|
|
71
|
+
// would have failed. The CLI's own verdict is the one that can be acted on.
|
|
72
|
+
const broken = status.botStatus === "verify_failed" || (status.verified === true && !status.botReady && !status.userReady);
|
|
73
|
+
if (broken) {
|
|
74
|
+
return {
|
|
75
|
+
...common,
|
|
76
|
+
userReady: false,
|
|
77
|
+
botReady: false,
|
|
78
|
+
connected: false,
|
|
79
|
+
step: 2,
|
|
80
|
+
rebuild: true,
|
|
81
|
+
message: `应用的凭据已经失效:${trimSentence(status.botMessage ?? "飞书拒绝了这次验证")}。点「连接飞书账号」会重新走一遍确认流程、建一个可用的应用。`,
|
|
82
|
+
...(status.hint ? { hint: status.hint } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (status.verified === false) {
|
|
86
|
+
return {
|
|
87
|
+
...common,
|
|
88
|
+
userReady: false,
|
|
89
|
+
botReady: false,
|
|
90
|
+
connected: false,
|
|
91
|
+
step: 2,
|
|
92
|
+
message: "没能连上飞书(本地配置读到了,但验证请求没出去)。检查一下网络,然后点「测试连接」重试。",
|
|
93
|
+
...(status.hint ? { hint: status.hint } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (!status.userReady) {
|
|
97
|
+
return {
|
|
98
|
+
...common,
|
|
99
|
+
userReady: false,
|
|
100
|
+
botReady: status.botReady,
|
|
101
|
+
connected: false,
|
|
102
|
+
step: 2,
|
|
103
|
+
...(status.identity ? { identity: status.identity } : {}),
|
|
104
|
+
message: "应用已经能用,但还没登录你的账号。点「连接飞书账号」在浏览器里授权一下,授权之后 Agent 就能以你的身份读写你自己的文档,不用逐篇加协作者。",
|
|
105
|
+
...(status.hint ? { hint: status.hint } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...common,
|
|
110
|
+
userReady: true,
|
|
111
|
+
botReady: status.botReady,
|
|
112
|
+
connected: true,
|
|
113
|
+
identity: status.identity ?? "user",
|
|
114
|
+
step: 3,
|
|
115
|
+
message: "已连接:Agent 会以你的身份读写飞书云文档。",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* CHECKS THE CONNECTION, and says whether it actually works.
|
|
120
|
+
*
|
|
121
|
+
* The status already verifies; this is the same answer shaped for a button, so that a person can
|
|
122
|
+
* ask "it still doesn't work, why?" and get the CLI's own verdict rather than a guess.
|
|
123
|
+
*/
|
|
124
|
+
async verify() {
|
|
125
|
+
const status = await this.status();
|
|
126
|
+
if (status.connected) {
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
message: status.message,
|
|
130
|
+
...(status.detail ? { detail: status.detail } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
message: status.message,
|
|
136
|
+
...(status.detail ? { detail: status.detail } : {}),
|
|
137
|
+
...(status.hint ? { hint: status.hint } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* STARTS THE CONNECT FLOW.
|
|
142
|
+
*
|
|
143
|
+
* Two steps, and which one runs is decided by what the CLI already has:
|
|
144
|
+
*
|
|
145
|
+
* - No application, or one whose credential no longer works: `config init --new` registers a
|
|
146
|
+
* fresh application through a browser confirmation. It BLOCKS until the person confirms, so it
|
|
147
|
+
* runs in the background and the URL is taken from its output. The broken case goes here too,
|
|
148
|
+
* because a new application is the one repair that needs no secret to be typed into a form.
|
|
149
|
+
* - A working application but no person: `auth login --no-wait --json` asks for a device code
|
|
150
|
+
* and hands back the URL to open. The code is kept here until the person says they are done.
|
|
151
|
+
*/
|
|
152
|
+
async connect() {
|
|
153
|
+
const status = await this.status();
|
|
154
|
+
if (!status.installed) {
|
|
155
|
+
return { ok: false, message: status.message };
|
|
156
|
+
}
|
|
157
|
+
// Step 2 is "connect the account"; the application part of it is missing or broken when the
|
|
158
|
+
// person has no application OR when the one they have was refused by Feishu.
|
|
159
|
+
if (!status.appId || status.rebuild) {
|
|
160
|
+
const verificationUri = await this.cli.startAppCreation();
|
|
161
|
+
this.pendingAppCreation = true;
|
|
162
|
+
return {
|
|
163
|
+
ok: true,
|
|
164
|
+
step: "app",
|
|
165
|
+
...(verificationUri ? { verificationUri } : {}),
|
|
166
|
+
message: verificationUri
|
|
167
|
+
? "在浏览器里打开这个链接确认一下,应用就建好了(CLI 会把应用和凭据存进系统钥匙串)。确认完点「我已完成」。"
|
|
168
|
+
: "CLI 没有打印出链接。请在终端里运行 lark-cli config init --new,按提示扫码确认,然后回到这里点「连接飞书账号」。",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const login = await this.cli.beginLogin();
|
|
173
|
+
this.pendingDeviceCode = login.deviceCode;
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
step: "login",
|
|
177
|
+
...(login.verificationUri ? { verificationUri: login.verificationUri } : {}),
|
|
178
|
+
deviceCode: login.deviceCode,
|
|
179
|
+
message: login.verificationUri
|
|
180
|
+
? "在浏览器里打开这个链接,用你的飞书账号确认授权,然后点「我已完成」。"
|
|
181
|
+
: "CLI 已经发起设备码授权。请在浏览器里确认,然后点「我已完成」。",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return { ok: false, message: messageOf(error), ...(hintOf(error) ? { hint: hintOf(error) } : {}) };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* FINISHES THE FLOW, once the person says they are done.
|
|
190
|
+
*
|
|
191
|
+
* TWO KINDS OF "DONE", and the difference is what the CLI is waiting for:
|
|
192
|
+
*
|
|
193
|
+
* - An application is being registered: `config init --new` finishes by itself once the person
|
|
194
|
+
* confirms in the browser, so there is nothing to hand back. The status is re-read and the
|
|
195
|
+
* answer is whether the application showed up.
|
|
196
|
+
* - A login is being authorized: the device code is handed back to the CLI, which completes the
|
|
197
|
+
* exchange and stores the token in the keychain. Super Pi never sees the token itself.
|
|
198
|
+
*/
|
|
199
|
+
async completeConnect() {
|
|
200
|
+
if (this.pendingAppCreation) {
|
|
201
|
+
this.pendingAppCreation = false;
|
|
202
|
+
const status = await this.status();
|
|
203
|
+
if (status.appId) {
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
message: "应用建好了。接下来点「连接飞书账号」授权你的账号,Agent 就能以你的身份读写文档了。",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
message: "还没看到应用建好。请确认浏览器里的确认已经提交(有时要等 CLI 写完钥匙串),然后再点一次「我已完成」。",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (!this.pendingDeviceCode) {
|
|
215
|
+
return { ok: false, message: "没有待完成的登录。点「连接飞书账号」重新开始。" };
|
|
216
|
+
}
|
|
217
|
+
const deviceCode = this.pendingDeviceCode;
|
|
218
|
+
this.pendingDeviceCode = undefined;
|
|
219
|
+
try {
|
|
220
|
+
const envelope = await this.cli.completeLogin(deviceCode);
|
|
221
|
+
if (envelope.ok === false) {
|
|
222
|
+
return { ok: false, message: errorMessage(envelope), ...(errorHint(envelope) ? { hint: errorHint(envelope) } : {}) };
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, message: "已连接。现在 Agent 能以你的身份读写飞书云文档了。" };
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
return { ok: false, message: messageOf(error), ...(hintOf(error) ? { hint: hintOf(error) } : {}) };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* READS A DOCUMENT.
|
|
232
|
+
*
|
|
233
|
+
* The CLI resolves a knowledge-base link itself, so a `/wiki/` URL is passed through untouched -
|
|
234
|
+
* and it answers with Markdown, which is the shape a model wants.
|
|
235
|
+
*/
|
|
236
|
+
async read(input) {
|
|
237
|
+
const document = await this.cli.fetchDocument(input.url);
|
|
238
|
+
return {
|
|
239
|
+
...(document.documentId ? { documentId: document.documentId } : {}),
|
|
240
|
+
...(document.revisionId !== undefined ? { revisionId: document.revisionId } : {}),
|
|
241
|
+
...(document.title ? { title: document.title } : {}),
|
|
242
|
+
url: input.url,
|
|
243
|
+
format: "markdown",
|
|
244
|
+
content: document.content,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* WRITES A DOCUMENT.
|
|
249
|
+
*
|
|
250
|
+
* THREE MODES, and the difference between them is who decides what survives:
|
|
251
|
+
*
|
|
252
|
+
* - `create` makes a new document. Nothing can be lost, because nothing was there.
|
|
253
|
+
* - `append` adds to the end. Nothing can be lost either.
|
|
254
|
+
* - `replace` replaces the whole document. This one CAN lose a person's work, so it needs the
|
|
255
|
+
* revision the caller read - the same rule the file panel's write lives by, and for the same
|
|
256
|
+
* reason: an agent told to "把这份报告改成正式版" would otherwise destroy a hand edit made an
|
|
257
|
+
* hour ago, with no refusal and no warning.
|
|
258
|
+
*
|
|
259
|
+
* THE REVISION IS THE ETAG. The CLI passes it as the base revision of the edit, and Feishu
|
|
260
|
+
* refuses the edit when the document has moved on - so the check is enforced by the API rather
|
|
261
|
+
* than by a comparison Super Pi makes and then has to keep honest.
|
|
262
|
+
*/
|
|
263
|
+
async write(input) {
|
|
264
|
+
const mode = input.mode ?? (input.url ? "append" : "create");
|
|
265
|
+
const content = String(input.content ?? "");
|
|
266
|
+
if (!content.trim())
|
|
267
|
+
throw new Error("要写入的内容是空的。");
|
|
268
|
+
if (mode === "create") {
|
|
269
|
+
const title = input.title?.trim() || titleFromMarkdown(content);
|
|
270
|
+
const created = await this.cli.createDocument(title, content);
|
|
271
|
+
return {
|
|
272
|
+
...(created.documentId ? { documentId: created.documentId } : {}),
|
|
273
|
+
...(created.revisionId !== undefined ? { revisionId: created.revisionId } : {}),
|
|
274
|
+
...(created.url ? { url: created.url } : {}),
|
|
275
|
+
mode,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (!input.url)
|
|
279
|
+
throw new Error("追加或覆盖需要给出文档链接或 token。");
|
|
280
|
+
if (mode === "replace") {
|
|
281
|
+
if (!input.baseRevisionId) {
|
|
282
|
+
throw new Error("覆盖前需要确认文档版本:先用 feishu_doc_read 读一次这篇文档,把它返回的 revisionId 作为 baseRevisionId 传入。覆盖会清空整篇文档,必须先看过。");
|
|
283
|
+
}
|
|
284
|
+
const revision = Number(input.baseRevisionId);
|
|
285
|
+
if (!Number.isFinite(revision)) {
|
|
286
|
+
throw new Error(`baseRevisionId 不是有效的版本号:${input.baseRevisionId}`);
|
|
287
|
+
}
|
|
288
|
+
const updated = await this.cli.updateDocument(input.url, "overwrite", content, revision);
|
|
289
|
+
return {
|
|
290
|
+
...(updated.revisionId !== undefined ? { revisionId: updated.revisionId } : {}),
|
|
291
|
+
url: input.url,
|
|
292
|
+
mode,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const updated = await this.cli.updateDocument(input.url, "append", content);
|
|
296
|
+
return {
|
|
297
|
+
...(updated.revisionId !== undefined ? { revisionId: updated.revisionId } : {}),
|
|
298
|
+
url: input.url,
|
|
299
|
+
mode,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* THE TOOL CALL, as the extension sends it.
|
|
304
|
+
*/
|
|
305
|
+
async tool(body) {
|
|
306
|
+
const request = (body ?? {});
|
|
307
|
+
const action = String(request.action ?? "read");
|
|
308
|
+
if (action === "read") {
|
|
309
|
+
return this.read({ url: String(request.url ?? "") });
|
|
310
|
+
}
|
|
311
|
+
if (action === "write") {
|
|
312
|
+
const mode = request.mode === "create" || request.mode === "append" || request.mode === "replace" ? request.mode : undefined;
|
|
313
|
+
return this.write({
|
|
314
|
+
...(request.url ? { url: String(request.url) } : {}),
|
|
315
|
+
content: String(request.content ?? ""),
|
|
316
|
+
...(mode ? { mode } : {}),
|
|
317
|
+
...(request.title ? { title: String(request.title) } : {}),
|
|
318
|
+
...(request.baseRevisionId ? { baseRevisionId: String(request.baseRevisionId) } : {}),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
throw new Error(`未知的飞书工具操作:${action}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* A SENTENCE WITHOUT ITS OWN FULL STOP.
|
|
326
|
+
*
|
|
327
|
+
* The CLI writes English sentences that end in a period, and this service writes Chinese ones that
|
|
328
|
+
* end in 。 — so a message pasted in verbatim reads "invalid.。点「重新连接」".
|
|
329
|
+
*/
|
|
330
|
+
function trimSentence(text) {
|
|
331
|
+
return text.trim().replace(/[.\s]+$/, "");
|
|
332
|
+
}
|
|
333
|
+
/** The message of a failure, whatever shape the failure has. */ function messageOf(error) {
|
|
334
|
+
if (error instanceof FeishuCliError)
|
|
335
|
+
return error.message;
|
|
336
|
+
if (error instanceof Error)
|
|
337
|
+
return error.message;
|
|
338
|
+
return String(error);
|
|
339
|
+
}
|
|
340
|
+
/** The hint of a failure, when it carries one. */
|
|
341
|
+
function hintOf(error) {
|
|
342
|
+
return error instanceof FeishuCliError ? error.hint : undefined;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* A TITLE FROM THE CONTENT, for a document created without one.
|
|
346
|
+
*
|
|
347
|
+
* The first heading is what a person would have called it, and a generated title is better than
|
|
348
|
+
* "未命名文档" only when there is something to take.
|
|
349
|
+
*/
|
|
350
|
+
function titleFromMarkdown(content) {
|
|
351
|
+
for (const line of content.split("\n")) {
|
|
352
|
+
const heading = /^#{1,6}\s+(.+?)\s*$/.exec(line.trim());
|
|
353
|
+
if (heading?.[1])
|
|
354
|
+
return heading[1].slice(0, 800);
|
|
355
|
+
}
|
|
356
|
+
const firstLine = content.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
|
|
357
|
+
return (firstLine ? firstLine.slice(0, 800) : "未命名文档");
|
|
358
|
+
}
|
|
@@ -1132,9 +1132,17 @@ export class RunService {
|
|
|
1132
1132
|
// `active: true` on every delta is not a field Pi sent - it is the conclusion Super Pi draws from
|
|
1133
1133
|
// having received one. Pi announces the END of reasoning with `thinking_end` and puts nothing on
|
|
1134
1134
|
// the delta, so these two halves together are the whole lifecycle.
|
|
1135
|
-
onThinking: (delta) => {
|
|
1135
|
+
onThinking: async (delta) => {
|
|
1136
1136
|
if (!delta)
|
|
1137
1137
|
return;
|
|
1138
|
+
// THINKING IS THE MODEL PRODUCING SOMETHING, SO THE TURN IS RUNNING. The contract above
|
|
1139
|
+
// says a turn moves from `waiting` to `running` once the model has produced ANY signal -
|
|
1140
|
+
// "regardless of shape" - and a thinking delta is the same signal as a text delta: it is Pi
|
|
1141
|
+
// yielding data back. Only two shapes promoted the turn (text and a tool-call notice), so a
|
|
1142
|
+
// model that thinks before it answers sat in `waiting` for the whole of its reasoning: the
|
|
1143
|
+
// panel said 「等待 LLM 返回」 while the model was plainly working, and - because the stop
|
|
1144
|
+
// button appears while a turn is in flight - the reader had no way to stop it for exactly
|
|
1145
|
+
// as long as the thinking lasted.
|
|
1138
1146
|
const newBlock = thinkingAnswerAt !== spoken || thinkingInterrupted;
|
|
1139
1147
|
if (newBlock) {
|
|
1140
1148
|
thinkingBlock++;
|
|
@@ -1145,6 +1153,12 @@ export class RunService {
|
|
|
1145
1153
|
this.store.setThinkingActiveLive(conversation.id, assistant.id, true);
|
|
1146
1154
|
thought += delta.length;
|
|
1147
1155
|
this.store.appendThinkingDeltaLive(conversation.id, assistant.id, delta, spoken, newBlock);
|
|
1156
|
+
// THE PROMOTION COMES AFTER THE DELTA IS IN THE STORE, so the state snapshot it broadcasts
|
|
1157
|
+
// carries the same thinking the client is about to receive as a live event. Promoting first
|
|
1158
|
+
// broadcast a snapshot WITHOUT this delta, and the client - which prefers the live body only
|
|
1159
|
+
// while the conversation is running - took that stale snapshot, lost the thinking block's
|
|
1160
|
+
// boundary, and dropped a tool call out of the block it belonged to.
|
|
1161
|
+
await promoteToRunning();
|
|
1148
1162
|
this.broadcaster.broadcast({ type: "message.thinking", conversationId: conversation.id, messageId: assistant.id, delta, active: true, at: spoken, blockIndex: thinkingBlock });
|
|
1149
1163
|
scheduleCheckpoint();
|
|
1150
1164
|
},
|