@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/server/runtime.d.ts
CHANGED
|
@@ -164,7 +164,11 @@ export interface AnswerInput {
|
|
|
164
164
|
onToolPreparing?(call: ToolCallPreparing): void;
|
|
165
165
|
/** Called while the primary model is emitting reasoning, so the UI can show live progress. A call means
|
|
166
166
|
* the model is STILL THINKING - there is no separate flag, because Pi does not send one. */
|
|
167
|
-
|
|
167
|
+
/** Called while the primary model is emitting reasoning, so the UI can show live progress. A call means
|
|
168
|
+
* the model is STILL THINKING - there is no separate flag, because Pi does not send one.
|
|
169
|
+
* The handler may be async: the reasoning signal is the same "the model is producing" signal as
|
|
170
|
+
* a text delta, and the caller waits for it so the state it writes is ordered with the delta. */
|
|
171
|
+
onThinking?(delta: string): void | Promise<void>;
|
|
168
172
|
/** Called when the reasoning phase ends - the only signal that it has. */
|
|
169
173
|
onThinkingEnd?(): void;
|
|
170
174
|
}
|
|
@@ -517,6 +521,14 @@ export declare function taskExtensionPath(from?: string): string;
|
|
|
517
521
|
* from `import.meta.dirname` reach the repository root both after `tsc -b` and under `tsx watch`.
|
|
518
522
|
*/
|
|
519
523
|
export declare function documentExtensionPath(from?: string): string;
|
|
524
|
+
/**
|
|
525
|
+
* The Feishu extension's file, resolved the same way the document extension's is.
|
|
526
|
+
*
|
|
527
|
+
* Named on both sides of the wire rather than imported by the extension, for the reason every one
|
|
528
|
+
* of these names is written out twice: `agents/` is loaded by Pi's module loader and `src/` by
|
|
529
|
+
* tsc, so a relative import across the two would resolve differently under each.
|
|
530
|
+
*/
|
|
531
|
+
export declare function feishuExtensionPath(from?: string): string;
|
|
520
532
|
export declare class PiRpcRuntime implements AgentRuntime {
|
|
521
533
|
private readonly sessionDir;
|
|
522
534
|
/**
|
|
@@ -552,6 +564,12 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
552
564
|
private readonly options;
|
|
553
565
|
private memory?;
|
|
554
566
|
setMemory(memory: ActiveMemory): void;
|
|
567
|
+
/**
|
|
568
|
+
* The memory and knowledge credentials minted for each LIVE session, so they can be released when
|
|
569
|
+
* the session goes. A credential whose session is gone must not keep working - that is exactly the
|
|
570
|
+
* shape of a secret with no owner.
|
|
571
|
+
*/
|
|
572
|
+
private readonly sessionExtensionTokens;
|
|
555
573
|
private knowledge?;
|
|
556
574
|
setKnowledge(knowledge: KnowledgeBase): void;
|
|
557
575
|
private readonly runs;
|
|
@@ -642,6 +660,8 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
642
660
|
private warnedMissingExtension;
|
|
643
661
|
/** Latches the "the document extension is missing" warning, for the same reason. */
|
|
644
662
|
private warnedMissingDocumentExtension;
|
|
663
|
+
/** Latches the "the Feishu extension is missing" warning, for the same reason. */
|
|
664
|
+
private warnedMissingFeishuExtension;
|
|
645
665
|
/** Latches the "Pi's SDK is not installed" warning, for the same reason. */
|
|
646
666
|
private warnedMissingSdk;
|
|
647
667
|
/**
|
|
@@ -709,6 +729,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
709
729
|
questionEndpoint?: string;
|
|
710
730
|
toolsEndpoint?: string;
|
|
711
731
|
documentsEndpoint?: string;
|
|
732
|
+
scheduledTaskEndpoint?: string;
|
|
712
733
|
uploadsDir?: string;
|
|
713
734
|
sessionFactory?: PiSessionFactory;
|
|
714
735
|
sessionMode?: "sdk" | "oneshot";
|
|
@@ -804,6 +825,16 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
804
825
|
* runtime with no document endpoint mounts nothing rather than a tool whose every call fails.
|
|
805
826
|
*/
|
|
806
827
|
private documentExtensionArgs;
|
|
828
|
+
/**
|
|
829
|
+
* The Feishu extension's spawn argument.
|
|
830
|
+
*
|
|
831
|
+
* MOUNTED UNCONDITIONALLY, for the same reason the document tools are: the endpoint is the gate,
|
|
832
|
+
* and the extension registers nothing without it. The credential is NOT the gate - a user who
|
|
833
|
+
* has not configured an application yet still gets the tools, because the error those tools
|
|
834
|
+
* return is the one that tells the user what to do, and a tool that is simply absent cannot say
|
|
835
|
+
* anything at all.
|
|
836
|
+
*/
|
|
837
|
+
private feishuExtensionArgs;
|
|
807
838
|
/**
|
|
808
839
|
* The five values the extension reads out of its own environment. See `task-tool.ts` for why they are
|
|
809
840
|
* written out on both sides rather than imported.
|
|
@@ -820,6 +851,21 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
820
851
|
* The TODO ENDPOINT is the one value here that is NOT per-turn derived from disk: it is this server's
|
|
821
852
|
* own address, so it is the same string on every turn and is read once from the constructor's options.
|
|
822
853
|
*/
|
|
854
|
+
/**
|
|
855
|
+
* THE MEMORY AND KNOWLEDGE CREDENTIALS FOR A NEW SESSION.
|
|
856
|
+
*
|
|
857
|
+
* Both extensions read their environment ONCE, when they are loaded, so these are minted where the
|
|
858
|
+
* session is born - the same place, and for the same reason, as the session's capability. The
|
|
859
|
+
* one-shot path has always minted them (per turn); the session path minted none, so the memory and
|
|
860
|
+
* knowledge extensions returned before registering a tool or injecting a prompt, and both features
|
|
861
|
+
* were missing in production - the SDK session path is the DEFAULT backend, and only the test
|
|
862
|
+
* scripts take the one-shot path (ledger TEST-039).
|
|
863
|
+
*
|
|
864
|
+
* The endpoints are published as global environment variables (index.ts), and they are repeated
|
|
865
|
+
* here so the extension's contract is stated in one place: an endpoint and a credential travel
|
|
866
|
+
* together, and the credential is what proves the caller.
|
|
867
|
+
*/
|
|
868
|
+
private mintSessionExtensionTokens;
|
|
823
869
|
/**
|
|
824
870
|
* The environment Super Pi's extensions read when they are loaded.
|
|
825
871
|
*
|
package/dist/server/runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ export { RpcChannel } from "./rpc-channel.js";
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { KNOWLEDGE_IMPORT_TOOL_NAME, KNOWLEDGE_TOOL_NAME } from "./knowledge.js";
|
|
6
6
|
import { DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME } from "./documents.js";
|
|
7
|
+
import { FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME } from "./feishu-cli.js";
|
|
7
8
|
import { WEB_FETCH_TOOL_NAME, WEB_SEARCH_TOOL_NAME } from "./web.js";
|
|
8
9
|
import { existsSync } from "node:fs";
|
|
9
10
|
import { mkdir, stat } from "node:fs/promises";
|
|
@@ -20,7 +21,7 @@ import { describeDeliveredUserMessage, describePiQueue, handleSessionEvent, Turn
|
|
|
20
21
|
// paths recognise a delivery identically. Re-exported because they were part of this module's surface.
|
|
21
22
|
export { describeDeliveredUserMessage, describePiQueue } from "./pi-session-events.js";
|
|
22
23
|
import { createPiSession, createSdkCatalog, piSdkAvailable, resolveTranscriptFile, superPiExtensionPaths } from "./pi-session.js";
|
|
23
|
-
import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
24
|
+
import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, SCHEDULED_TASK_ENDPOINT_ENV, SCHEDULED_TASK_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, rosterPayload, } from "./task-tool.js";
|
|
24
25
|
import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
|
|
25
26
|
import { loadConversationImages } from "./upload.js";
|
|
26
27
|
import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
|
|
@@ -248,6 +249,16 @@ export function taskExtensionPath(from = import.meta.dirname) {
|
|
|
248
249
|
export function documentExtensionPath(from = import.meta.dirname) {
|
|
249
250
|
return resolve(from, "../../agents/super-pi-documents/index.ts");
|
|
250
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* The Feishu extension's file, resolved the same way the document extension's is.
|
|
254
|
+
*
|
|
255
|
+
* Named on both sides of the wire rather than imported by the extension, for the reason every one
|
|
256
|
+
* of these names is written out twice: `agents/` is loaded by Pi's module loader and `src/` by
|
|
257
|
+
* tsc, so a relative import across the two would resolve differently under each.
|
|
258
|
+
*/
|
|
259
|
+
export function feishuExtensionPath(from = import.meta.dirname) {
|
|
260
|
+
return resolve(from, "../../agents/super-pi-feishu/index.ts");
|
|
261
|
+
}
|
|
251
262
|
/** Bridges a Pi session to Super Pi through Pi's documented JSONL RPC protocol. */
|
|
252
263
|
/**
|
|
253
264
|
* The tool's name as far as the stream has revealed it so far, or an empty string while the model is
|
|
@@ -278,6 +289,12 @@ export class PiRpcRuntime {
|
|
|
278
289
|
options;
|
|
279
290
|
memory;
|
|
280
291
|
setMemory(memory) { this.memory = memory; }
|
|
292
|
+
/**
|
|
293
|
+
* The memory and knowledge credentials minted for each LIVE session, so they can be released when
|
|
294
|
+
* the session goes. A credential whose session is gone must not keep working - that is exactly the
|
|
295
|
+
* shape of a secret with no owner.
|
|
296
|
+
*/
|
|
297
|
+
sessionExtensionTokens = new Map();
|
|
281
298
|
knowledge;
|
|
282
299
|
setKnowledge(knowledge) { this.knowledge = knowledge; }
|
|
283
300
|
runs = new Map();
|
|
@@ -368,6 +385,8 @@ export class PiRpcRuntime {
|
|
|
368
385
|
warnedMissingExtension = false;
|
|
369
386
|
/** Latches the "the document extension is missing" warning, for the same reason. */
|
|
370
387
|
warnedMissingDocumentExtension = false;
|
|
388
|
+
/** Latches the "the Feishu extension is missing" warning, for the same reason. */
|
|
389
|
+
warnedMissingFeishuExtension = false;
|
|
371
390
|
/** Latches the "Pi's SDK is not installed" warning, for the same reason. */
|
|
372
391
|
warnedMissingSdk = false;
|
|
373
392
|
/**
|
|
@@ -453,7 +472,16 @@ export class PiRpcRuntime {
|
|
|
453
472
|
// ABOUT that session and stop being true with it.
|
|
454
473
|
this.sessionHosts.delete(conversationId);
|
|
455
474
|
this.reportedSessionFiles.delete(conversationId);
|
|
456
|
-
|
|
475
|
+
// The session held these credentials too, so they die with it.
|
|
476
|
+
const tokens = this.sessionExtensionTokens.get(conversationId);
|
|
477
|
+
if (tokens) {
|
|
478
|
+
if (tokens.memoryToken)
|
|
479
|
+
this.memory?.release(tokens.memoryToken);
|
|
480
|
+
if (tokens.knowledgeToken)
|
|
481
|
+
this.knowledge?.release(tokens.knowledgeToken);
|
|
482
|
+
this.sessionExtensionTokens.delete(conversationId);
|
|
483
|
+
}
|
|
484
|
+
}, options.sessionFactory ?? createPiSession, (conversationId) => mintCapability(conversationId), () => this.modelEpoch, (conversationId) => this.mintSessionExtensionTokens(conversationId));
|
|
457
485
|
}
|
|
458
486
|
/**
|
|
459
487
|
* The configuration of the session that was built for a conversation, for tests to inspect.
|
|
@@ -577,6 +605,28 @@ export class PiRpcRuntime {
|
|
|
577
605
|
}
|
|
578
606
|
return ["--extension", path];
|
|
579
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* The Feishu extension's spawn argument.
|
|
610
|
+
*
|
|
611
|
+
* MOUNTED UNCONDITIONALLY, for the same reason the document tools are: the endpoint is the gate,
|
|
612
|
+
* and the extension registers nothing without it. The credential is NOT the gate - a user who
|
|
613
|
+
* has not configured an application yet still gets the tools, because the error those tools
|
|
614
|
+
* return is the one that tells the user what to do, and a tool that is simply absent cannot say
|
|
615
|
+
* anything at all.
|
|
616
|
+
*/
|
|
617
|
+
feishuExtensionArgs() {
|
|
618
|
+
if (!process.env.SUPER_PI_FEISHU_ENDPOINT)
|
|
619
|
+
return [];
|
|
620
|
+
const path = feishuExtensionPath();
|
|
621
|
+
if (!existsSync(path)) {
|
|
622
|
+
if (!this.warnedMissingFeishuExtension) {
|
|
623
|
+
this.warnedMissingFeishuExtension = true;
|
|
624
|
+
console.warn(`PiRpcRuntime: ${path} does not exist, so the primary agent cannot read or write Feishu cloud documents.`);
|
|
625
|
+
}
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
return ["--extension", path];
|
|
629
|
+
}
|
|
580
630
|
/**
|
|
581
631
|
* The five values the extension reads out of its own environment. See `task-tool.ts` for why they are
|
|
582
632
|
* written out on both sides rather than imported.
|
|
@@ -593,6 +643,37 @@ export class PiRpcRuntime {
|
|
|
593
643
|
* The TODO ENDPOINT is the one value here that is NOT per-turn derived from disk: it is this server's
|
|
594
644
|
* own address, so it is the same string on every turn and is read once from the constructor's options.
|
|
595
645
|
*/
|
|
646
|
+
/**
|
|
647
|
+
* THE MEMORY AND KNOWLEDGE CREDENTIALS FOR A NEW SESSION.
|
|
648
|
+
*
|
|
649
|
+
* Both extensions read their environment ONCE, when they are loaded, so these are minted where the
|
|
650
|
+
* session is born - the same place, and for the same reason, as the session's capability. The
|
|
651
|
+
* one-shot path has always minted them (per turn); the session path minted none, so the memory and
|
|
652
|
+
* knowledge extensions returned before registering a tool or injecting a prompt, and both features
|
|
653
|
+
* were missing in production - the SDK session path is the DEFAULT backend, and only the test
|
|
654
|
+
* scripts take the one-shot path (ledger TEST-039).
|
|
655
|
+
*
|
|
656
|
+
* The endpoints are published as global environment variables (index.ts), and they are repeated
|
|
657
|
+
* here so the extension's contract is stated in one place: an endpoint and a credential travel
|
|
658
|
+
* together, and the credential is what proves the caller.
|
|
659
|
+
*/
|
|
660
|
+
async mintSessionExtensionTokens(conversationId) {
|
|
661
|
+
const minted = {};
|
|
662
|
+
const env = {};
|
|
663
|
+
if (this.memory && (await this.memory.configuration()).enabled) {
|
|
664
|
+
minted.memoryToken = this.memory.issue(conversationId);
|
|
665
|
+
env.SUPER_PI_MEMORY_TOKEN = minted.memoryToken;
|
|
666
|
+
env.SUPER_PI_MEMORY_ENDPOINT = process.env.SUPER_PI_MEMORY_ENDPOINT ?? "";
|
|
667
|
+
}
|
|
668
|
+
if (this.knowledge && this.knowledge.enabled()) {
|
|
669
|
+
minted.knowledgeToken = this.knowledge.issue(conversationId);
|
|
670
|
+
env.SUPER_PI_KNOWLEDGE_TOKEN = minted.knowledgeToken;
|
|
671
|
+
env.SUPER_PI_KNOWLEDGE_ENDPOINT = process.env.SUPER_PI_KNOWLEDGE_ENDPOINT ?? "";
|
|
672
|
+
}
|
|
673
|
+
if (minted.memoryToken || minted.knowledgeToken)
|
|
674
|
+
this.sessionExtensionTokens.set(conversationId, minted);
|
|
675
|
+
return env;
|
|
676
|
+
}
|
|
596
677
|
/**
|
|
597
678
|
* The environment Super Pi's extensions read when they are loaded.
|
|
598
679
|
*
|
|
@@ -612,6 +693,7 @@ export class PiRpcRuntime {
|
|
|
612
693
|
[TASK_CONVERSATION_ENV]: conversationId,
|
|
613
694
|
[TASK_DEPTH_ENV]: "0",
|
|
614
695
|
[TODO_ENDPOINT_ENV]: this.options.todoEndpoint ?? "",
|
|
696
|
+
[SCHEDULED_TASK_ENDPOINT_ENV]: this.options.scheduledTaskEndpoint ?? "",
|
|
615
697
|
[WORKFLOW_ENDPOINT_ENV]: this.options.workflowEndpoint ?? "",
|
|
616
698
|
SUPER_PI_QUESTION_ENDPOINT: this.options.questionEndpoint ?? "",
|
|
617
699
|
SUPER_PI_TOOLS_ENDPOINT: this.options.toolsEndpoint ?? "",
|
|
@@ -623,6 +705,11 @@ export class PiRpcRuntime {
|
|
|
623
705
|
SUPER_PI_DOCUMENTS_ENDPOINT: this.options.documentsEndpoint ?? "",
|
|
624
706
|
SUPER_PI_DOCUMENTS_CONVERSATION: conversationId,
|
|
625
707
|
SUPER_PI_WEB_ENDPOINT: process.env.SUPER_PI_WEB_ENDPOINT ?? "",
|
|
708
|
+
// The Feishu tools' endpoint and the conversation they act for. The CREDENTIAL is not here:
|
|
709
|
+
// it is the session's own capability, the same value the task dispatch uses, because a
|
|
710
|
+
// second credential for one session would be a second thing to rotate and to release.
|
|
711
|
+
SUPER_PI_FEISHU_ENDPOINT: process.env.SUPER_PI_FEISHU_ENDPOINT ?? "",
|
|
712
|
+
SUPER_PI_FEISHU_CONVERSATION: conversationId,
|
|
626
713
|
};
|
|
627
714
|
}
|
|
628
715
|
/**
|
|
@@ -865,13 +952,10 @@ export class PiRpcRuntime {
|
|
|
865
952
|
// pushes the terminal item when Pi settles, and the consumer below reads it. This promise
|
|
866
953
|
// waits for that signal, and the race with the prompt's own failure is what keeps a turn that
|
|
867
954
|
// never settles from hanging forever - a hung turn is a conversation that never answers.
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
// never produce anything and the next message can never be sent.
|
|
873
|
-
new Promise((resolvePromise) => setTimeout(resolvePromise, 5_000)),
|
|
874
|
-
]);
|
|
955
|
+
// `prompt()` only means Pi accepted the request. Do not end this generator on a wall-clock
|
|
956
|
+
// timeout: the session may still be streaming or executing a tool, and ending here drops
|
|
957
|
+
// subsequent events while falsely allowing the next prompt onto a busy session.
|
|
958
|
+
await queue.settledSignal;
|
|
875
959
|
traceLatency(conversation.id, "session.settled", latencyStartedAt);
|
|
876
960
|
// THE REVERT BOUNDARY, recorded at the same moment the one-shot path records it: the turn has
|
|
877
961
|
// settled, so every entry this turn wrote - the user message included - is in the transcript.
|
|
@@ -1055,7 +1139,7 @@ export class PiRpcRuntime {
|
|
|
1055
1139
|
* because `toolFlags` treats an explicit false as outranking a host extra.
|
|
1056
1140
|
*/
|
|
1057
1141
|
extensionToolNames() {
|
|
1058
|
-
const names = [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, "question", "tool_register"];
|
|
1142
|
+
const names = [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register"];
|
|
1059
1143
|
if (this.memory)
|
|
1060
1144
|
names.push("memory", "skill_manage", "session_search");
|
|
1061
1145
|
if (this.knowledge)
|
|
@@ -1067,6 +1151,12 @@ export class PiRpcRuntime {
|
|
|
1067
1151
|
// would otherwise get a loaded-but-undeclared tool - broken with no error to explain it.
|
|
1068
1152
|
if (this.options.documentsEndpoint)
|
|
1069
1153
|
names.push(DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME);
|
|
1154
|
+
// The Feishu tools are named whenever the endpoint is configured, for the same reason the
|
|
1155
|
+
// document tools are: the endpoint is what decides whether the extension registers them, and
|
|
1156
|
+
// an agent that declares `tools: {"*": false, read: true}` would otherwise get a
|
|
1157
|
+
// loaded-but-undeclared tool - broken with no error to explain it.
|
|
1158
|
+
if (process.env.SUPER_PI_FEISHU_ENDPOINT)
|
|
1159
|
+
names.push(FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME);
|
|
1070
1160
|
return names;
|
|
1071
1161
|
}
|
|
1072
1162
|
/**
|
|
@@ -1097,7 +1187,7 @@ export class PiRpcRuntime {
|
|
|
1097
1187
|
if (segment.kind === "text")
|
|
1098
1188
|
yield segment.text;
|
|
1099
1189
|
else if (segment.kind === "thinking")
|
|
1100
|
-
input.onThinking?.(segment.text);
|
|
1190
|
+
await input.onThinking?.(segment.text);
|
|
1101
1191
|
else
|
|
1102
1192
|
input.onThinkingEnd?.();
|
|
1103
1193
|
}
|
|
@@ -1223,6 +1313,11 @@ export class PiRpcRuntime {
|
|
|
1223
1313
|
const webExtension = resolve(import.meta.dirname, "../../agents/super-pi-web/index.ts");
|
|
1224
1314
|
const webEnabled = !memoryReview && !knowledgeRerank && existsSync(webExtension) && !!process.env.SUPER_PI_WEB_ENDPOINT;
|
|
1225
1315
|
const webArgs = webEnabled ? ["--extension", webExtension] : [];
|
|
1316
|
+
// The Feishu tools are mounted for every ordinary turn, and NOT for a review or a rerank: a
|
|
1317
|
+
// background reflection has no business reading a company's cloud documents, and a reranker
|
|
1318
|
+
// must not be able to reach the tools it is ranking.
|
|
1319
|
+
const feishuEnabled = !memoryReview && !knowledgeRerank && this.feishuExtensionArgs().length > 0;
|
|
1320
|
+
const feishuArgs = feishuEnabled ? this.feishuExtensionArgs() : [];
|
|
1226
1321
|
// The document tools are mounted for every ordinary turn, and NOT for a review or a rerank: a
|
|
1227
1322
|
// background reflection has no business reading a PDF it was not asked about, and a reranker
|
|
1228
1323
|
// must not be able to reach the tools it is ranking.
|
|
@@ -1232,7 +1327,7 @@ export class PiRpcRuntime {
|
|
|
1232
1327
|
// conversation, so minting a second one for the same conversation would replace the first and
|
|
1233
1328
|
// leave the task dispatch holding a token the server has already forgotten. Both the task
|
|
1234
1329
|
// extension and the document extension therefore share this one value.
|
|
1235
|
-
const oneShotCapability = (extensionArgs.length || documentsEnabled || webEnabled) ? mintCapability(conversation.id) : undefined;
|
|
1330
|
+
const oneShotCapability = (extensionArgs.length || documentsEnabled || webEnabled || feishuEnabled) ? mintCapability(conversation.id) : undefined;
|
|
1236
1331
|
// BOTH extension tools, or neither. Naming one while the other goes unmentioned would be a lie in a
|
|
1237
1332
|
// different direction: Pi filters its registry through `--tools`, so an agent that declares
|
|
1238
1333
|
// `tools: {"*": false, read: true}` gets whichever of the two is on this line and silently not the
|
|
@@ -1245,13 +1340,13 @@ export class PiRpcRuntime {
|
|
|
1245
1340
|
// allowlist and naming a tool Pi has not registered would change the spawn line for every other
|
|
1246
1341
|
// mode. The prompt asks for a bare JSON array and nothing else; if the model spends a turn on a
|
|
1247
1342
|
// tool call anyway the reranker times out and falls back to the lexical order.
|
|
1248
|
-
const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, "question", "tool_register"] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : [])] });
|
|
1343
|
+
const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : knowledgeRerank ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, SCHEDULED_TASK_TOOL_NAME, "question", "tool_register"] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : []), ...(knowledgeEnabled ? [KNOWLEDGE_TOOL_NAME, KNOWLEDGE_IMPORT_TOOL_NAME] : []), ...(documentsEnabled ? [DOCUMENT_READ_TOOL_NAME, DOCUMENT_WRITE_TOOL_NAME] : []), ...(webEnabled ? [WEB_SEARCH_TOOL_NAME, WEB_FETCH_TOOL_NAME] : []), ...(feishuEnabled ? [FEISHU_READ_TOOL_NAME, FEISHU_WRITE_TOOL_NAME] : [])] });
|
|
1249
1344
|
// Refused here, with the reason, rather than handed to the spawn: an ENOENT from `spawn` reads as
|
|
1250
1345
|
// "the app is broken" and hides the one fact that matters, which is where it looked for Pi.
|
|
1251
1346
|
if (!this.pi.found)
|
|
1252
1347
|
throw new Error(this.pi.reason);
|
|
1253
1348
|
// npm exposes global CLIs as .cmd shims on Windows; Node requires a shell to launch those shims.
|
|
1254
|
-
const child = spawn(shellQuote(this.pi.command), ["--mode", "rpc", "--session-dir", this.sessionDir, "--session-id", conversation.id, ...extensionArgs, ...memoryArgs, ...knowledgeArgs, ...documentArgs, ...webArgs, ...agentArgs], {
|
|
1349
|
+
const child = spawn(shellQuote(this.pi.command), ["--mode", "rpc", "--session-dir", this.sessionDir, "--session-id", conversation.id, ...extensionArgs, ...memoryArgs, ...knowledgeArgs, ...documentArgs, ...webArgs, ...feishuArgs, ...agentArgs], {
|
|
1255
1350
|
cwd: childCwd,
|
|
1256
1351
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1257
1352
|
shell: process.platform === "win32",
|
|
@@ -1259,7 +1354,7 @@ export class PiRpcRuntime {
|
|
|
1259
1354
|
// every turn would silently replace the child's environment with whatever THIS process has at
|
|
1260
1355
|
// spawn time. Inheriting is what an omitted `env` already does, so the two paths differ in nothing
|
|
1261
1356
|
// but the four documented additions.
|
|
1262
|
-
...((extensionArgs.length || memoryEnabled || knowledgeEnabled || documentsEnabled || webEnabled) ? { env: { ...process.env, ...(oneShotCapability ? await this.oneShotExtensionEnv(conversation.id, oneShotCapability) : {}), ...(memoryEnabled ? {
|
|
1357
|
+
...((extensionArgs.length || memoryEnabled || knowledgeEnabled || documentsEnabled || webEnabled || feishuEnabled) ? { env: { ...process.env, ...(oneShotCapability ? await this.oneShotExtensionEnv(conversation.id, oneShotCapability) : {}), ...(memoryEnabled ? {
|
|
1263
1358
|
SUPER_PI_MEMORY_ENDPOINT: process.env.SUPER_PI_MEMORY_ENDPOINT,
|
|
1264
1359
|
SUPER_PI_MEMORY_TOKEN: memoryToken,
|
|
1265
1360
|
SUPER_PI_MEMORY_REVIEW: memoryReview ? "1" : "0",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ModelSelection } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* WHEN A SCHEDULED TASK RUNS.
|
|
4
|
+
*
|
|
5
|
+
* FOUR SHAPES, AND NOT A CRON EXPRESSION. A cron string would bring a parser, a timezone story and a
|
|
6
|
+
* daylight-saving story to a panel that runs on the same machine as the server - and the two shapes
|
|
7
|
+
* people actually ask for are "every N minutes" and "every day at HH:MM". The set is closed on purpose:
|
|
8
|
+
* a schedule that cannot be expressed here is not a schedule the panel can edit, and the panel is the
|
|
9
|
+
* only place a user can see what they are agreeing to.
|
|
10
|
+
*
|
|
11
|
+
* - `interval`: every N minutes. The interval is anchored on the last run, not on the wall clock, so a
|
|
12
|
+
* task that takes four minutes to run does not slowly drift later every cycle.
|
|
13
|
+
* - `daily`: every day at a local `HH:MM`.
|
|
14
|
+
* - `weekly`: on the listed weekdays (0 = Sunday) at a local `HH:MM`.
|
|
15
|
+
* - `once`: at one instant. The task is finished after it fires.
|
|
16
|
+
*
|
|
17
|
+
* THE TIME IS LOCAL, not UTC and not a named zone, because the server and the panel are the same
|
|
18
|
+
* machine for every installation that exists. A "09:00" that means something different in the panel
|
|
19
|
+
* than in the scheduler would be the worst possible bug in this feature: it would fire at the wrong
|
|
20
|
+
* hour and look like it had fired at the right one.
|
|
21
|
+
*/
|
|
22
|
+
export type ScheduleSpec = {
|
|
23
|
+
kind: "interval";
|
|
24
|
+
everyMinutes: number;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "daily";
|
|
27
|
+
time: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "weekly";
|
|
30
|
+
weekdays: number[];
|
|
31
|
+
time: string;
|
|
32
|
+
} | {
|
|
33
|
+
kind: "once";
|
|
34
|
+
at: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* ONE SCHEDULED TASK: a prompt, a model to run it with, and when to run it.
|
|
38
|
+
*
|
|
39
|
+
* `conversationId` is the task's OWN conversation, created lazily on the first run. The task never runs
|
|
40
|
+
* in somebody else's conversation: a scheduled run that landed in the user's working session would
|
|
41
|
+
* interleave with what they were doing, and would change that session's model out from under it.
|
|
42
|
+
*/
|
|
43
|
+
export type ScheduledTask = {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
/** The instruction, sent VERBATIM as the user message of the task's conversation. */
|
|
47
|
+
prompt: string;
|
|
48
|
+
/** The workspace the task belongs to: it decides the working directory and the defaults. */
|
|
49
|
+
workspaceId: string;
|
|
50
|
+
/** The task's own conversation, created on the first run. Absent until then. */
|
|
51
|
+
conversationId?: string;
|
|
52
|
+
/** Overrides the workspace's default agent. */
|
|
53
|
+
agentId?: string;
|
|
54
|
+
/** Overrides the workspace's default model. A `provider`/`modelId` PAIR, never half of one. */
|
|
55
|
+
model?: ModelSelection;
|
|
56
|
+
schedule: ScheduleSpec;
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
/** When the task fires next, in epoch milliseconds. Absent for a finished one-shot task. */
|
|
59
|
+
nextRunAt?: number;
|
|
60
|
+
lastRunAt?: number;
|
|
61
|
+
lastRunStatus?: "completed" | "failed" | "skipped";
|
|
62
|
+
lastRunMessage?: string;
|
|
63
|
+
/** Whether a person or the agent created it. Both are editable by both. */
|
|
64
|
+
createdBy: "user" | "agent";
|
|
65
|
+
createdAt: number;
|
|
66
|
+
updatedAt: number;
|
|
67
|
+
};
|
|
68
|
+
/** The shortest interval a task may ask for. A one-minute floor is what makes a task a task. */
|
|
69
|
+
export declare const MIN_INTERVAL_MINUTES = 1;
|
|
70
|
+
/** The longest interval: a week. Anything longer is a calendar arrangement, not an interval. */
|
|
71
|
+
export declare const MAX_INTERVAL_MINUTES: number;
|
|
72
|
+
/**
|
|
73
|
+
* Reads a schedule out of whatever a caller sent, or explains why it cannot.
|
|
74
|
+
*
|
|
75
|
+
* THE SERVER OWNS THIS, and the extension on the other side of the wire deliberately does NOT have a
|
|
76
|
+
* copy of it. The extension hands over what the model wrote; this function decides what it means. A
|
|
77
|
+
* second implementation on the extension side would be a second opinion about the same wire, and the two
|
|
78
|
+
* would disagree the first time a model wrote a schedule in a shape one of them accepted and the other
|
|
79
|
+
* did not - with the model's mistake showing up as a server error it cannot explain.
|
|
80
|
+
*/
|
|
81
|
+
export declare function normalizeSchedule(raw: unknown): ScheduleSpec;
|
|
82
|
+
/**
|
|
83
|
+
* WHEN THE TASK FIRES NEXT, given a moment to look forward from.
|
|
84
|
+
*
|
|
85
|
+
* `undefined` means "never again": a one-shot task whose moment has passed.
|
|
86
|
+
*
|
|
87
|
+
* The daily and weekly cases walk forward day by day in LOCAL time rather than adding 24 hours, so a
|
|
88
|
+
* schedule at 09:00 stays at 09:00 across a daylight-saving change instead of drifting to 08:00 or
|
|
89
|
+
* 10:00 for half the year.
|
|
90
|
+
*/
|
|
91
|
+
export declare function nextRunAt(spec: ScheduleSpec, from: number): number | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* THE SCHEDULE IN WORDS, for the panel and for the text the agent reads back.
|
|
94
|
+
*
|
|
95
|
+
* One implementation for both readers, because two descriptions of one schedule would eventually say
|
|
96
|
+
* two different things - and the agent's description is what the user is told when the agent says
|
|
97
|
+
* "I set up a daily task". If the two disagreed, the user would find out from the clock.
|
|
98
|
+
*/
|
|
99
|
+
export declare function describeSchedule(spec: ScheduleSpec): string;
|
|
100
|
+
/** A moment as `YYYY-MM-DD HH:MM`, in local time. */
|
|
101
|
+
export declare function formatMoment(ms: number): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** The shortest interval a task may ask for. A one-minute floor is what makes a task a task. */
|
|
2
|
+
export const MIN_INTERVAL_MINUTES = 1;
|
|
3
|
+
/** The longest interval: a week. Anything longer is a calendar arrangement, not an interval. */
|
|
4
|
+
export const MAX_INTERVAL_MINUTES = 60 * 24 * 7;
|
|
5
|
+
/**
|
|
6
|
+
* Reads a schedule out of whatever a caller sent, or explains why it cannot.
|
|
7
|
+
*
|
|
8
|
+
* THE SERVER OWNS THIS, and the extension on the other side of the wire deliberately does NOT have a
|
|
9
|
+
* copy of it. The extension hands over what the model wrote; this function decides what it means. A
|
|
10
|
+
* second implementation on the extension side would be a second opinion about the same wire, and the two
|
|
11
|
+
* would disagree the first time a model wrote a schedule in a shape one of them accepted and the other
|
|
12
|
+
* did not - with the model's mistake showing up as a server error it cannot explain.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeSchedule(raw) {
|
|
15
|
+
if (!raw || typeof raw !== "object")
|
|
16
|
+
throw new Error("周期必须是一个对象:{\"kind\":\"interval\",\"everyMinutes\":30} 这样写。");
|
|
17
|
+
const value = raw;
|
|
18
|
+
const kind = typeof value.kind === "string" ? value.kind.trim() : "";
|
|
19
|
+
if (kind === "interval") {
|
|
20
|
+
const everyMinutes = Number(value.everyMinutes);
|
|
21
|
+
if (!Number.isFinite(everyMinutes) || !Number.isInteger(everyMinutes))
|
|
22
|
+
throw new Error("每隔 N 分钟需要一个整数分钟数。");
|
|
23
|
+
if (everyMinutes < MIN_INTERVAL_MINUTES || everyMinutes > MAX_INTERVAL_MINUTES) {
|
|
24
|
+
throw new Error(`间隔分钟数要在 ${MIN_INTERVAL_MINUTES} 到 ${MAX_INTERVAL_MINUTES} 之间。`);
|
|
25
|
+
}
|
|
26
|
+
return { kind: "interval", everyMinutes };
|
|
27
|
+
}
|
|
28
|
+
if (kind === "daily")
|
|
29
|
+
return { kind: "daily", time: normalizeTime(value.time) };
|
|
30
|
+
if (kind === "weekly") {
|
|
31
|
+
const weekdays = normalizeWeekdays(value.weekdays);
|
|
32
|
+
return { kind: "weekly", weekdays, time: normalizeTime(value.time) };
|
|
33
|
+
}
|
|
34
|
+
if (kind === "once") {
|
|
35
|
+
const at = Number(value.at);
|
|
36
|
+
if (!Number.isFinite(at) || at <= 0)
|
|
37
|
+
throw new Error("一次性任务需要一个触发时刻(毫秒时间戳)。");
|
|
38
|
+
return { kind: "once", at };
|
|
39
|
+
}
|
|
40
|
+
throw new Error("周期只能是 interval(每隔 N 分钟)、daily(每天)、weekly(每周)或 once(仅一次)。");
|
|
41
|
+
}
|
|
42
|
+
/** `HH:MM`, twenty-four hour, leading zero or not. The only time shape this feature accepts. */
|
|
43
|
+
function normalizeTime(raw) {
|
|
44
|
+
if (typeof raw !== "string")
|
|
45
|
+
throw new Error("触发时刻要写成 \"HH:MM\",比如 \"09:00\"。");
|
|
46
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(raw.trim());
|
|
47
|
+
if (!match)
|
|
48
|
+
throw new Error("触发时刻要写成 \"HH:MM\",比如 \"09:00\"。");
|
|
49
|
+
const hours = Number(match[1]);
|
|
50
|
+
const minutes = Number(match[2]);
|
|
51
|
+
if (hours > 23 || minutes > 59)
|
|
52
|
+
throw new Error("触发时刻不是一个真实的时间。");
|
|
53
|
+
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
|
|
54
|
+
}
|
|
55
|
+
/** The weekdays of a weekly schedule, sorted and deduplicated. An empty list means every day. */
|
|
56
|
+
function normalizeWeekdays(raw) {
|
|
57
|
+
if (!Array.isArray(raw))
|
|
58
|
+
throw new Error("每周任务需要给出星期几,比如 [1,3] 表示周一和周三。");
|
|
59
|
+
const days = Array.from(new Set(raw.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0 && item <= 6)));
|
|
60
|
+
if (!days.length)
|
|
61
|
+
throw new Error("每周任务至少要有一天。");
|
|
62
|
+
return days.sort((a, b) => a - b);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* WHEN THE TASK FIRES NEXT, given a moment to look forward from.
|
|
66
|
+
*
|
|
67
|
+
* `undefined` means "never again": a one-shot task whose moment has passed.
|
|
68
|
+
*
|
|
69
|
+
* The daily and weekly cases walk forward day by day in LOCAL time rather than adding 24 hours, so a
|
|
70
|
+
* schedule at 09:00 stays at 09:00 across a daylight-saving change instead of drifting to 08:00 or
|
|
71
|
+
* 10:00 for half the year.
|
|
72
|
+
*/
|
|
73
|
+
export function nextRunAt(spec, from) {
|
|
74
|
+
if (spec.kind === "interval")
|
|
75
|
+
return from + spec.everyMinutes * 60_000;
|
|
76
|
+
if (spec.kind === "once")
|
|
77
|
+
return spec.at > from ? spec.at : undefined;
|
|
78
|
+
if (spec.kind === "daily")
|
|
79
|
+
return nextDailyAt(spec.time, from);
|
|
80
|
+
return nextWeeklyAt(spec.weekdays, spec.time, from);
|
|
81
|
+
}
|
|
82
|
+
/** The next moment at `HH:MM` strictly after `from`, in local time. */
|
|
83
|
+
function nextDailyAt(time, from) {
|
|
84
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
85
|
+
const candidate = new Date(from);
|
|
86
|
+
candidate.setHours(hours, minutes, 0, 0);
|
|
87
|
+
if (candidate.getTime() <= from)
|
|
88
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
89
|
+
return candidate.getTime();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The next moment at `HH:MM` on one of `weekdays`, strictly after `from`, in local time.
|
|
93
|
+
*
|
|
94
|
+
* At most eight days are examined, which covers every weekday list including an empty one: an empty
|
|
95
|
+
* list means "every day", and the daily case is a special case of it.
|
|
96
|
+
*/
|
|
97
|
+
function nextWeeklyAt(weekdays, time, from) {
|
|
98
|
+
const days = weekdays.length ? weekdays : [0, 1, 2, 3, 4, 5, 6];
|
|
99
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
100
|
+
for (let offset = 0; offset <= 7; offset += 1) {
|
|
101
|
+
const candidate = new Date(from);
|
|
102
|
+
candidate.setDate(candidate.getDate() + offset);
|
|
103
|
+
candidate.setHours(hours, minutes, 0, 0);
|
|
104
|
+
if (candidate.getTime() <= from)
|
|
105
|
+
continue;
|
|
106
|
+
if (days.includes(candidate.getDay()))
|
|
107
|
+
return candidate.getTime();
|
|
108
|
+
}
|
|
109
|
+
// Unreachable: within eight days every weekday in a full week comes round. The fallback keeps the
|
|
110
|
+
// function total rather than returning `undefined` for a shape that cannot happen.
|
|
111
|
+
return nextDailyAt(time, from);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* THE SCHEDULE IN WORDS, for the panel and for the text the agent reads back.
|
|
115
|
+
*
|
|
116
|
+
* One implementation for both readers, because two descriptions of one schedule would eventually say
|
|
117
|
+
* two different things - and the agent's description is what the user is told when the agent says
|
|
118
|
+
* "I set up a daily task". If the two disagreed, the user would find out from the clock.
|
|
119
|
+
*/
|
|
120
|
+
export function describeSchedule(spec) {
|
|
121
|
+
if (spec.kind === "interval")
|
|
122
|
+
return `每隔 ${spec.everyMinutes} 分钟`;
|
|
123
|
+
if (spec.kind === "daily")
|
|
124
|
+
return `每天 ${spec.time}`;
|
|
125
|
+
if (spec.kind === "weekly")
|
|
126
|
+
return `每周${spec.weekdays.map(weekdayName).join("、")} ${spec.time}`;
|
|
127
|
+
return `仅一次:${formatMoment(spec.at)}`;
|
|
128
|
+
}
|
|
129
|
+
/** 0 = Sunday, which is what `Date.getDay()` says. */
|
|
130
|
+
function weekdayName(day) {
|
|
131
|
+
return ["日", "一", "二", "三", "四", "五", "六"][day] ?? "?";
|
|
132
|
+
}
|
|
133
|
+
/** A moment as `YYYY-MM-DD HH:MM`, in local time. */
|
|
134
|
+
export function formatMoment(ms) {
|
|
135
|
+
const date = new Date(ms);
|
|
136
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
137
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
138
|
+
}
|
|
@@ -42,6 +42,14 @@ export interface ServerContext {
|
|
|
42
42
|
web?: import("./web.js").WebService;
|
|
43
43
|
/** The agent-facing document tools. Optional so the server can start without them. */
|
|
44
44
|
documentService?: import("./application/document-service.js").DocumentService;
|
|
45
|
+
/**
|
|
46
|
+
* THE FEISHU TOOLS.
|
|
47
|
+
*
|
|
48
|
+
* Optional because the credential may be missing, not because the feature may be missing: the
|
|
49
|
+
* service is always constructed, and a call made before the credential is configured is refused
|
|
50
|
+
* with the message that says what to do - which a missing service could not say.
|
|
51
|
+
*/
|
|
52
|
+
feishu?: import("./application/feishu-service.js").FeishuService;
|
|
45
53
|
paths: ServerPaths;
|
|
46
54
|
store: Store;
|
|
47
55
|
runtime: AgentRuntime;
|
|
@@ -55,6 +63,14 @@ export interface ServerContext {
|
|
|
55
63
|
catalogService: CatalogService;
|
|
56
64
|
/** The providers a person added through the settings window. */
|
|
57
65
|
providerService: ProviderService;
|
|
66
|
+
/**
|
|
67
|
+
* The scheduled tasks and the clock behind them.
|
|
68
|
+
*
|
|
69
|
+
* In the context because TWO layers need it: the command handler routes the panel's five commands
|
|
70
|
+
* into it, and the HTTP layer exposes the agent-facing endpoint that lets a running turn create a
|
|
71
|
+
* task of its own.
|
|
72
|
+
*/
|
|
73
|
+
scheduledTaskService?: import("./application/scheduled-task-service.js").ScheduledTaskService;
|
|
58
74
|
execService: ExecService;
|
|
59
75
|
toolRegistry: ToolRegistry;
|
|
60
76
|
toolRunner: ToolRunner;
|