@evandu/super-pi 0.1.132 → 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/dist/.super-pi-check-inputs.json +4 -0
- package/dist/client/.super-pi-client-sources.json +5 -5
- package/dist/client/assets/{CodeMirrorEditor-ktyL2PcX.js → CodeMirrorEditor-CZZfGD0U.js} +2 -2
- package/dist/client/assets/{dist-8pdzh6h1.js → dist-0FH-oFco.js} +1 -1
- package/dist/client/assets/{dist-Bg9zLfex.js → dist-4Pts9xPO.js} +1 -1
- package/dist/client/assets/{dist-Hj_dWgPr.js → dist-B5ruVtUG.js} +1 -1
- package/dist/client/assets/{dist-Cu75NyQ0.js → dist-BJinarhP.js} +1 -1
- package/dist/client/assets/{dist-CMscy2Nn.js → dist-BffE00vI.js} +1 -1
- package/dist/client/assets/{dist-CoszOk5k.js → dist-BtTFCNVQ.js} +1 -1
- package/dist/client/assets/{dist-DvHghuGd.js → dist-C2nuBCP4.js} +1 -1
- package/dist/client/assets/{dist-BAJCrbnX.js → dist-CApAttfj.js} +1 -1
- package/dist/client/assets/{dist-CPLCQDqy.js → dist-CE5BnwJa.js} +1 -1
- package/dist/client/assets/{dist-BRjGckmQ.js → dist-CFPXj4ws.js} +1 -1
- package/dist/client/assets/{dist-BjZUYxLd.js → dist-CUKaDTyO.js} +1 -1
- package/dist/client/assets/{dist-vsMBLoxV.js → dist-CYD2SqmG.js} +1 -1
- package/dist/client/assets/{dist-B8OZ-n_C.js → dist-CxsunljU.js} +1 -1
- package/dist/client/assets/{dist-D_Zdnhm8.js → dist-D4x1XTQb.js} +1 -1
- package/dist/client/assets/{dist-Zhdkfvng.js → dist-DHqqpxo4.js} +1 -1
- package/dist/client/assets/{dist-DNZMXYNq.js → dist-DOYWHMIq.js} +1 -1
- package/dist/client/assets/{dist-BJi5HmH4.js → dist-DQf5nNxa.js} +1 -1
- package/dist/client/assets/{dist-CgmISAMr.js → dist-DbbBgSwL.js} +1 -1
- package/dist/client/assets/{dist-Bv6P2Rcp.js → dist-LFfokYJs.js} +1 -1
- package/dist/client/assets/{dist-C6iAHGvb.js → dist-aL2NGji_.js} +1 -1
- package/dist/client/assets/{dist--igDAzRS.js → dist-azDN8a65.js} +1 -1
- package/dist/client/assets/{dist-60usx__o.js → dist-fYp8Xwrx.js} +1 -1
- package/dist/client/assets/index-YEN4ysHF.js +100 -0
- package/dist/client/index.html +1 -1
- package/dist/server/agents.js +21 -1
- package/dist/server/application/feishu-service.d.ts +57 -10
- package/dist/server/application/feishu-service.js +118 -24
- package/dist/server/application/run-service.js +15 -1
- package/dist/server/feishu-cli.d.ts +18 -2
- package/dist/server/feishu-cli.js +46 -12
- 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/http-server.js +20 -0
- package/dist/server/persistence/sqlite-state-repository.js +18 -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/runtime.d.ts +26 -1
- package/dist/server/runtime.js +52 -9
- package/package.json +10 -1
- package/dist/client/assets/index-ChyemF2I.js +0 -100
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-YEN4ysHF.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/index-D3MEG8uH.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
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, "");
|
|
@@ -23,8 +23,31 @@ export type FeishuStatus = {
|
|
|
23
23
|
identity?: "user" | "bot";
|
|
24
24
|
/** Whether the agent can read and write documents right now. */
|
|
25
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;
|
|
26
34
|
/** What to do next, in one sentence. */
|
|
27
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;
|
|
28
51
|
};
|
|
29
52
|
/** The outcome of starting the connect flow. */
|
|
30
53
|
export type FeishuConnectResult = {
|
|
@@ -50,35 +73,59 @@ export declare class FeishuService {
|
|
|
50
73
|
private readonly cli;
|
|
51
74
|
/** The device code of a login that is waiting for the person to authorize it. */
|
|
52
75
|
private pendingDeviceCode?;
|
|
76
|
+
/** Whether an application registration is running and waiting for the browser confirmation. */
|
|
77
|
+
private pendingAppCreation;
|
|
53
78
|
constructor(cli?: FeishuCli);
|
|
54
79
|
/**
|
|
55
80
|
* WHAT THE CLI CAN DO RIGHT NOW.
|
|
56
81
|
*
|
|
57
|
-
* The
|
|
82
|
+
* The states a person can be in, and what each one means for the agent:
|
|
58
83
|
*
|
|
59
84
|
* - The CLI is not installed: nothing works, and the fix is one command.
|
|
60
85
|
* - The CLI has no application: `config init --new` creates one through a browser confirmation,
|
|
61
86
|
* so the developer console is never visited.
|
|
62
|
-
* - The
|
|
63
|
-
*
|
|
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.
|
|
64
92
|
*/
|
|
65
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
|
+
}>;
|
|
66
106
|
/**
|
|
67
107
|
* STARTS THE CONNECT FLOW.
|
|
68
108
|
*
|
|
69
109
|
* Two steps, and which one runs is decided by what the CLI already has:
|
|
70
110
|
*
|
|
71
|
-
* - No application: `config init --new` registers
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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.
|
|
75
117
|
*/
|
|
76
118
|
connect(): Promise<FeishuConnectResult>;
|
|
77
119
|
/**
|
|
78
|
-
* FINISHES THE
|
|
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:
|
|
79
123
|
*
|
|
80
|
-
*
|
|
81
|
-
* in the
|
|
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.
|
|
82
129
|
*/
|
|
83
130
|
completeConnect(): Promise<FeishuConnectResult>;
|
|
84
131
|
/**
|
|
@@ -17,19 +17,24 @@ export class FeishuService {
|
|
|
17
17
|
cli;
|
|
18
18
|
/** The device code of a login that is waiting for the person to authorize it. */
|
|
19
19
|
pendingDeviceCode;
|
|
20
|
+
/** Whether an application registration is running and waiting for the browser confirmation. */
|
|
21
|
+
pendingAppCreation = false;
|
|
20
22
|
constructor(cli = new FeishuCli()) {
|
|
21
23
|
this.cli = cli;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* WHAT THE CLI CAN DO RIGHT NOW.
|
|
25
27
|
*
|
|
26
|
-
* The
|
|
28
|
+
* The states a person can be in, and what each one means for the agent:
|
|
27
29
|
*
|
|
28
30
|
* - The CLI is not installed: nothing works, and the fix is one command.
|
|
29
31
|
* - The CLI has no application: `config init --new` creates one through a browser confirmation,
|
|
30
32
|
* so the developer console is never visited.
|
|
31
|
-
* - The
|
|
32
|
-
*
|
|
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.
|
|
33
38
|
*/
|
|
34
39
|
async status() {
|
|
35
40
|
const status = await this.cli.status();
|
|
@@ -39,59 +44,121 @@ export class FeishuService {
|
|
|
39
44
|
userReady: false,
|
|
40
45
|
botReady: false,
|
|
41
46
|
connected: false,
|
|
42
|
-
|
|
47
|
+
step: 1,
|
|
48
|
+
message: "还没有安装官方飞书 CLI。在终端里跑一次 npm i -g @larksuite/cli 就行,装完回到这里点「连接飞书账号」。",
|
|
49
|
+
...(status.hint ? { hint: status.hint } : {}),
|
|
43
50
|
};
|
|
44
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
|
+
};
|
|
45
59
|
if (!status.appId) {
|
|
46
60
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
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,
|
|
49
76
|
userReady: false,
|
|
50
77
|
botReady: false,
|
|
51
78
|
connected: false,
|
|
52
|
-
|
|
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 } : {}),
|
|
53
94
|
};
|
|
54
95
|
}
|
|
55
96
|
if (!status.userReady) {
|
|
56
97
|
return {
|
|
57
|
-
|
|
58
|
-
...(status.version ? { version: status.version } : {}),
|
|
59
|
-
appId: status.appId,
|
|
98
|
+
...common,
|
|
60
99
|
userReady: false,
|
|
61
100
|
botReady: status.botReady,
|
|
62
101
|
connected: false,
|
|
102
|
+
step: 2,
|
|
63
103
|
...(status.identity ? { identity: status.identity } : {}),
|
|
64
|
-
message: "
|
|
104
|
+
message: "应用已经能用,但还没登录你的账号。点「连接飞书账号」在浏览器里授权一下,授权之后 Agent 就能以你的身份读写你自己的文档,不用逐篇加协作者。",
|
|
105
|
+
...(status.hint ? { hint: status.hint } : {}),
|
|
65
106
|
};
|
|
66
107
|
}
|
|
67
108
|
return {
|
|
68
|
-
|
|
69
|
-
...(status.version ? { version: status.version } : {}),
|
|
70
|
-
appId: status.appId,
|
|
109
|
+
...common,
|
|
71
110
|
userReady: true,
|
|
72
111
|
botReady: status.botReady,
|
|
73
112
|
connected: true,
|
|
74
113
|
identity: status.identity ?? "user",
|
|
114
|
+
step: 3,
|
|
75
115
|
message: "已连接:Agent 会以你的身份读写飞书云文档。",
|
|
76
116
|
};
|
|
77
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
|
+
}
|
|
78
140
|
/**
|
|
79
141
|
* STARTS THE CONNECT FLOW.
|
|
80
142
|
*
|
|
81
143
|
* Two steps, and which one runs is decided by what the CLI already has:
|
|
82
144
|
*
|
|
83
|
-
* - No application: `config init --new` registers
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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.
|
|
87
151
|
*/
|
|
88
152
|
async connect() {
|
|
89
153
|
const status = await this.status();
|
|
90
154
|
if (!status.installed) {
|
|
91
155
|
return { ok: false, message: status.message };
|
|
92
156
|
}
|
|
93
|
-
|
|
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) {
|
|
94
160
|
const verificationUri = await this.cli.startAppCreation();
|
|
161
|
+
this.pendingAppCreation = true;
|
|
95
162
|
return {
|
|
96
163
|
ok: true,
|
|
97
164
|
step: "app",
|
|
@@ -119,12 +186,31 @@ export class FeishuService {
|
|
|
119
186
|
}
|
|
120
187
|
}
|
|
121
188
|
/**
|
|
122
|
-
* FINISHES THE
|
|
189
|
+
* FINISHES THE FLOW, once the person says they are done.
|
|
123
190
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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.
|
|
126
198
|
*/
|
|
127
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
|
+
}
|
|
128
214
|
if (!this.pendingDeviceCode) {
|
|
129
215
|
return { ok: false, message: "没有待完成的登录。点「连接飞书账号」重新开始。" };
|
|
130
216
|
}
|
|
@@ -235,8 +321,16 @@ export class FeishuService {
|
|
|
235
321
|
throw new Error(`未知的飞书工具操作:${action}`);
|
|
236
322
|
}
|
|
237
323
|
}
|
|
238
|
-
/**
|
|
239
|
-
|
|
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) {
|
|
240
334
|
if (error instanceof FeishuCliError)
|
|
241
335
|
return error.message;
|
|
242
336
|
if (error instanceof Error)
|
|
@@ -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
|
},
|
|
@@ -64,6 +64,16 @@ export type FeishuCliStatus = {
|
|
|
64
64
|
identity?: FeishuIdentity;
|
|
65
65
|
/** Feishu's own "what to do about it", when the CLI had something to say. */
|
|
66
66
|
hint?: string;
|
|
67
|
+
/** The CLI's own summary of the situation, when it has one. */
|
|
68
|
+
note?: string;
|
|
69
|
+
/** The state of each identity, as the CLI words it: ready, missing, verify_failed, ... */
|
|
70
|
+
botStatus?: string;
|
|
71
|
+
userStatus?: string;
|
|
72
|
+
/** What the CLI says about each identity, in its own words. */
|
|
73
|
+
userMessage?: string;
|
|
74
|
+
botMessage?: string;
|
|
75
|
+
/** Whether the check actually reached Feishu, rather than only reading the local credential. */
|
|
76
|
+
verified?: boolean;
|
|
67
77
|
};
|
|
68
78
|
/** The outcome of a CLI call, in the shape this product speaks in. */
|
|
69
79
|
export declare class FeishuCliError extends Error {
|
|
@@ -139,8 +149,14 @@ export declare class FeishuCli {
|
|
|
139
149
|
/**
|
|
140
150
|
* WHAT THE CLI KNOWS ABOUT ITSELF.
|
|
141
151
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
152
|
+
* THE VERIFY IS ON, and that is the whole point of asking the CLI at all. Without `--verify` the
|
|
153
|
+
* CLI only reads its local credential and reports the application as "ready" — which is what it
|
|
154
|
+
* said on a machine whose App Secret had expired, and which would have left the panel claiming
|
|
155
|
+
* everything was fine while every document call failed. With `--verify` the CLI asks Feishu, and
|
|
156
|
+
* the answer distinguishes the states a person can act on: `ready`, `missing`, `verify_failed`.
|
|
157
|
+
*
|
|
158
|
+
* When the verification cannot reach Feishu at all (no network), the plain status is read
|
|
159
|
+
* instead, and `verified: false` says so rather than pretending.
|
|
144
160
|
*/
|
|
145
161
|
status(): Promise<FeishuCliStatus>;
|
|
146
162
|
/**
|
|
@@ -198,38 +198,72 @@ export class FeishuCli {
|
|
|
198
198
|
/**
|
|
199
199
|
* WHAT THE CLI KNOWS ABOUT ITSELF.
|
|
200
200
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
201
|
+
* THE VERIFY IS ON, and that is the whole point of asking the CLI at all. Without `--verify` the
|
|
202
|
+
* CLI only reads its local credential and reports the application as "ready" — which is what it
|
|
203
|
+
* said on a machine whose App Secret had expired, and which would have left the panel claiming
|
|
204
|
+
* everything was fine while every document call failed. With `--verify` the CLI asks Feishu, and
|
|
205
|
+
* the answer distinguishes the states a person can act on: `ready`, `missing`, `verify_failed`.
|
|
206
|
+
*
|
|
207
|
+
* When the verification cannot reach Feishu at all (no network), the plain status is read
|
|
208
|
+
* instead, and `verified: false` says so rather than pretending.
|
|
203
209
|
*/
|
|
204
210
|
async status() {
|
|
205
211
|
const version = await this.version();
|
|
206
212
|
if (!version)
|
|
207
213
|
return { installed: false, userReady: false, botReady: false };
|
|
214
|
+
const read = async (args) => this.run(args, { timeoutMs: 30_000 });
|
|
208
215
|
let envelope;
|
|
216
|
+
let verified = true;
|
|
209
217
|
try {
|
|
210
|
-
envelope = await
|
|
218
|
+
envelope = await read(["auth", "status", "--verify", "--json"]);
|
|
219
|
+
// The CLI answers a failed verification with an error envelope. That is not the same as a
|
|
220
|
+
// credential that Feishu refused - it means the question was never answered - so the local
|
|
221
|
+
// status is read instead and the panel is told that Feishu was never asked.
|
|
222
|
+
if (envelope.ok === false) {
|
|
223
|
+
envelope = await read(["auth", "status", "--json"]);
|
|
224
|
+
verified = false;
|
|
225
|
+
}
|
|
211
226
|
}
|
|
212
227
|
catch (error) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
228
|
+
try {
|
|
229
|
+
envelope = await read(["auth", "status", "--json"]);
|
|
230
|
+
verified = false;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return {
|
|
234
|
+
installed: true,
|
|
235
|
+
version,
|
|
236
|
+
userReady: false,
|
|
237
|
+
botReady: false,
|
|
238
|
+
...(error instanceof FeishuCliError ? { hint: error.message } : {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
220
241
|
}
|
|
221
242
|
const identities = pick(envelope.data ?? envelope, "identities");
|
|
222
243
|
const user = identities?.user;
|
|
223
244
|
const bot = identities?.bot;
|
|
224
245
|
const identity = asString(envelope.identity) ?? pick(envelope.data ?? envelope, "identity");
|
|
246
|
+
const appId = asString(pick(envelope.data ?? envelope, "appId", "app_id"));
|
|
247
|
+
const note = asString(pick(envelope.data ?? envelope, "note"));
|
|
248
|
+
// THE HINT OF THE IDENTITY THAT IS FAILING. Both identities carry a hint, and the wrong one
|
|
249
|
+
// sends the person to the wrong command: the application's hint says "log in" when the real
|
|
250
|
+
// problem is that the credential was refused, and the user's hint says "check the credential"
|
|
251
|
+
// when all that is missing is a login.
|
|
252
|
+
const failing = bot?.available !== true ? bot : user;
|
|
225
253
|
return {
|
|
226
254
|
installed: true,
|
|
227
255
|
version,
|
|
228
|
-
...(
|
|
256
|
+
...(appId ? { appId } : {}),
|
|
229
257
|
userReady: user?.available === true,
|
|
230
258
|
botReady: bot?.available === true,
|
|
231
259
|
...(identity === "user" || identity === "bot" ? { identity } : {}),
|
|
232
|
-
...(asString(user?.
|
|
260
|
+
...(asString(user?.status) ? { userStatus: asString(user?.status) } : {}),
|
|
261
|
+
...(asString(bot?.status) ? { botStatus: asString(bot?.status) } : {}),
|
|
262
|
+
...(asString(user?.message) ? { userMessage: asString(user?.message) } : {}),
|
|
263
|
+
...(asString(bot?.message) ? { botMessage: asString(bot?.message) } : {}),
|
|
264
|
+
...(asString(failing?.hint) ? { hint: asString(failing?.hint) } : {}),
|
|
265
|
+
...(note ? { note } : {}),
|
|
266
|
+
verified,
|
|
233
267
|
};
|
|
234
268
|
}
|
|
235
269
|
/**
|
|
@@ -0,0 +1,68 @@
|
|
|
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;
|