cli-claw-kit 0.0.1
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/LICENSE +21 -0
- package/README.md +245 -0
- package/config/default-groups.json +1 -0
- package/config/global-agents-md.template.md +37 -0
- package/config/mount-allowlist.json +11 -0
- package/container/Dockerfile +160 -0
- package/container/agent-runner/dist/.tsbuildinfo +1 -0
- package/container/agent-runner/dist/agent-definitions.js +22 -0
- package/container/agent-runner/dist/channel-prefixes.js +16 -0
- package/container/agent-runner/dist/codex-config.js +29 -0
- package/container/agent-runner/dist/image-detector.js +96 -0
- package/container/agent-runner/dist/index.js +2587 -0
- package/container/agent-runner/dist/mcp-tools.js +1076 -0
- package/container/agent-runner/dist/stream-event.types.js +5 -0
- package/container/agent-runner/dist/stream-processor.js +867 -0
- package/container/agent-runner/dist/types.js +6 -0
- package/container/agent-runner/dist/utils.js +115 -0
- package/container/agent-runner/package.json +36 -0
- package/container/agent-runner/prompts/security-rules.md +31 -0
- package/container/agent-runner/src/agent-definitions.ts +27 -0
- package/container/agent-runner/src/channel-prefixes.ts +16 -0
- package/container/agent-runner/src/codex-config.ts +40 -0
- package/container/agent-runner/src/image-detector.ts +116 -0
- package/container/agent-runner/src/index.ts +3107 -0
- package/container/agent-runner/src/mcp-tools.ts +1295 -0
- package/container/agent-runner/src/stream-event.types.ts +10 -0
- package/container/agent-runner/src/stream-processor.ts +932 -0
- package/container/agent-runner/src/types.ts +75 -0
- package/container/agent-runner/src/utils.ts +114 -0
- package/container/agent-runner/tsconfig.json +17 -0
- package/container/build.sh +28 -0
- package/container/entrypoint.sh +64 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/install-skill/SKILL.md +64 -0
- package/container/skills/post-test-cleanup/SKILL.md +121 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/agent-output-parser.js +459 -0
- package/dist/app-root.js +52 -0
- package/dist/assistant-meta-footer.js +1 -0
- package/dist/auth.js +91 -0
- package/dist/billing.js +694 -0
- package/dist/channel-prefixes.js +16 -0
- package/dist/cli.js +86 -0
- package/dist/commands.js +79 -0
- package/dist/config.js +120 -0
- package/dist/container-runner.js +981 -0
- package/dist/daily-summary.js +210 -0
- package/dist/db.js +3683 -0
- package/dist/dingtalk.js +1347 -0
- package/dist/feishu-markdown-style.js +97 -0
- package/dist/feishu-streaming-card.js +1875 -0
- package/dist/feishu.js +1628 -0
- package/dist/file-manager.js +270 -0
- package/dist/group-queue.js +1070 -0
- package/dist/group-runtime.js +35 -0
- package/dist/host-workspace-cwd.js +85 -0
- package/dist/im-channel.js +384 -0
- package/dist/im-command-utils.js +142 -0
- package/dist/im-downloader.js +45 -0
- package/dist/im-manager.js +527 -0
- package/dist/im-utils.js +53 -0
- package/dist/image-detector.js +96 -0
- package/dist/index.js +5828 -0
- package/dist/logger.js +22 -0
- package/dist/mcp-utils.js +66 -0
- package/dist/message-attachments.js +69 -0
- package/dist/message-notifier.js +36 -0
- package/dist/middleware/auth.js +85 -0
- package/dist/mount-security.js +315 -0
- package/dist/permissions.js +67 -0
- package/dist/project-memory.js +6 -0
- package/dist/provider-pool.js +189 -0
- package/dist/qq.js +826 -0
- package/dist/reset-admin.js +42 -0
- package/dist/routes/admin.js +543 -0
- package/dist/routes/agent-definitions.js +241 -0
- package/dist/routes/agents.js +533 -0
- package/dist/routes/auth.js +675 -0
- package/dist/routes/billing.js +490 -0
- package/dist/routes/browse.js +210 -0
- package/dist/routes/bug-report.js +387 -0
- package/dist/routes/config.js +1868 -0
- package/dist/routes/files.js +671 -0
- package/dist/routes/groups.js +1367 -0
- package/dist/routes/mcp-servers.js +320 -0
- package/dist/routes/memory.js +523 -0
- package/dist/routes/monitor.js +307 -0
- package/dist/routes/skills.js +777 -0
- package/dist/routes/tasks.js +509 -0
- package/dist/routes/usage.js +64 -0
- package/dist/routes/workspace-config.js +458 -0
- package/dist/runtime-build.js +112 -0
- package/dist/runtime-command-handler.js +189 -0
- package/dist/runtime-command-registry.js +1 -0
- package/dist/runtime-config.js +1777 -0
- package/dist/runtime-identity.js +52 -0
- package/dist/schemas.js +590 -0
- package/dist/script-runner.js +64 -0
- package/dist/sdk-query.js +82 -0
- package/dist/skill-utils.js +145 -0
- package/dist/sqlite-compat.js +19 -0
- package/dist/stream-event.types.js +5 -0
- package/dist/streaming-runtime-meta.js +29 -0
- package/dist/task-scheduler.js +695 -0
- package/dist/task-utils.js +13 -0
- package/dist/telegram-pairing.js +59 -0
- package/dist/telegram.js +897 -0
- package/dist/terminal-manager.js +307 -0
- package/dist/tool-step-display.js +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +85 -0
- package/dist/web-context.js +161 -0
- package/dist/web.js +1377 -0
- package/dist/wechat-crypto.js +182 -0
- package/dist/wechat.js +589 -0
- package/dist/workspace-runtime-reset.js +35 -0
- package/package.json +107 -0
- package/shared/assistant-meta-footer.ts +127 -0
- package/shared/channel-prefixes.ts +16 -0
- package/shared/dist/assistant-meta-footer.d.ts +29 -0
- package/shared/dist/assistant-meta-footer.js +85 -0
- package/shared/dist/channel-prefixes.d.ts +4 -0
- package/shared/dist/channel-prefixes.js +16 -0
- package/shared/dist/image-detector.d.ts +20 -0
- package/shared/dist/image-detector.js +96 -0
- package/shared/dist/runtime-command-registry.d.ts +38 -0
- package/shared/dist/runtime-command-registry.js +185 -0
- package/shared/dist/stream-event.d.ts +65 -0
- package/shared/dist/stream-event.js +8 -0
- package/shared/dist/tool-step-display.d.ts +4 -0
- package/shared/dist/tool-step-display.js +11 -0
- package/shared/image-detector.ts +116 -0
- package/shared/runtime-command-registry.ts +252 -0
- package/shared/stream-event.ts +67 -0
- package/shared/tool-step-display.ts +21 -0
- package/shared/tsconfig.json +24 -0
- package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
- package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
- package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
- package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
- package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
- package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
- package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
- package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
- package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
- package/web/dist/assets/band-CquvqAHh.js +1 -0
- package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
- package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
- package/web/dist/assets/channel-BOVj73LR.js +1 -0
- package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
- package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
- package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
- package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
- package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
- package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
- package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
- package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
- package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
- package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
- package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
- package/web/dist/assets/clone-BmaCesfa.js +1 -0
- package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
- package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
- package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
- package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
- package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
- package/web/dist/assets/error-CGD5mp5f.js +1 -0
- package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
- package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
- package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
- package/web/dist/assets/graph-CeAEckur.js +1 -0
- package/web/dist/assets/index-CPnL1_qC.js +768 -0
- package/web/dist/assets/index-DVevCbcO.css +10 -0
- package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
- package/web/dist/assets/init-Dmth1JHB.js +1 -0
- package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
- package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
- package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
- package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
- package/web/dist/assets/linear-DiaJloY5.js +1 -0
- package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
- package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
- package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
- package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
- package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
- package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
- package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
- package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
- package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
- package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
- package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
- package/web/dist/assets/square-0CqMX1Q3.js +11 -0
- package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
- package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
- package/web/dist/assets/step-D51IIHGA.js +1 -0
- package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
- package/web/dist/assets/time-O8zIGux3.js +1 -0
- package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
- package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
- package/web/dist/assets/utils-KGAn0XTg.js +11 -0
- package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
- package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
- package/web/dist/assets/zap-_hKJYy7J.js +6 -0
- package/web/dist/favicon.svg +332 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin.woff2 +0 -0
- package/web/dist/icons/README.md +20 -0
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-128.png +0 -0
- package/web/dist/icons/icon-144.png +0 -0
- package/web/dist/icons/icon-152.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-192.svg +332 -0
- package/web/dist/icons/icon-384.png +0 -0
- package/web/dist/icons/icon-48.png +0 -0
- package/web/dist/icons/icon-512-maskable.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-512.svg +332 -0
- package/web/dist/icons/icon-72.png +0 -0
- package/web/dist/icons/icon-96.png +0 -0
- package/web/dist/icons/loading-logo.svg +332 -0
- package/web/dist/icons/logo-1024.png +0 -0
- package/web/dist/icons/logo-icon.svg +332 -0
- package/web/dist/icons/logo-text.svg +332 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +1 -0
- package/web/dist/registerSW.js +1 -0
- package/web/dist/sw.js +1 -0
- package/web/dist/workbox-08d6266a.js +1 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output parsing and process lifecycle logic for container-runner.
|
|
3
|
+
* Extracted from runContainerAgent() and runHostAgent() to eliminate duplication.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { getSystemSettings } from './runtime-config.js';
|
|
8
|
+
import { logger } from './logger.js';
|
|
9
|
+
// Sentinel markers for robust output parsing (must match agent-runner)
|
|
10
|
+
export const OUTPUT_START_MARKER = '---CLI_CLAW_OUTPUT_START---';
|
|
11
|
+
export const OUTPUT_END_MARKER = '---CLI_CLAW_OUTPUT_END---';
|
|
12
|
+
export function createStdoutParserState() {
|
|
13
|
+
return {
|
|
14
|
+
stdout: '',
|
|
15
|
+
stdoutTruncated: false,
|
|
16
|
+
parseBuffer: '',
|
|
17
|
+
newSessionId: undefined,
|
|
18
|
+
outputChain: Promise.resolve(),
|
|
19
|
+
lastErrorOutput: null,
|
|
20
|
+
hasSuccessOutput: false,
|
|
21
|
+
hasClosedOutput: false,
|
|
22
|
+
hasInterruptedOutput: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function attachStdoutHandler(stream, state, opts) {
|
|
26
|
+
stream.on('data', (data) => {
|
|
27
|
+
const chunk = data.toString();
|
|
28
|
+
// Always accumulate for logging
|
|
29
|
+
if (!state.stdoutTruncated) {
|
|
30
|
+
const remaining = getSystemSettings().containerMaxOutputSize - state.stdout.length;
|
|
31
|
+
if (chunk.length > remaining) {
|
|
32
|
+
state.stdout += chunk.slice(0, remaining);
|
|
33
|
+
state.stdoutTruncated = true;
|
|
34
|
+
logger.warn({ group: opts.groupName, size: state.stdout.length }, `${opts.label} stdout truncated due to size limit`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
state.stdout += chunk;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Stream-parse for output markers
|
|
41
|
+
if (opts.onOutput) {
|
|
42
|
+
state.parseBuffer += chunk;
|
|
43
|
+
const MAX_PARSE_BUFFER = 10 * 1024 * 1024; // 10MB
|
|
44
|
+
if (state.parseBuffer.length > MAX_PARSE_BUFFER) {
|
|
45
|
+
logger.warn({ group: opts.groupName }, 'Parse buffer overflow, truncating');
|
|
46
|
+
const lastMarkerIdx = state.parseBuffer.lastIndexOf(OUTPUT_START_MARKER);
|
|
47
|
+
state.parseBuffer =
|
|
48
|
+
lastMarkerIdx >= 0
|
|
49
|
+
? state.parseBuffer.slice(lastMarkerIdx)
|
|
50
|
+
: state.parseBuffer.slice(-512);
|
|
51
|
+
}
|
|
52
|
+
let startIdx;
|
|
53
|
+
while ((startIdx = state.parseBuffer.indexOf(OUTPUT_START_MARKER)) !== -1) {
|
|
54
|
+
const endIdx = state.parseBuffer.indexOf(OUTPUT_END_MARKER, startIdx);
|
|
55
|
+
if (endIdx === -1)
|
|
56
|
+
break; // Incomplete pair, wait for more data
|
|
57
|
+
const jsonStr = state.parseBuffer
|
|
58
|
+
.slice(startIdx + OUTPUT_START_MARKER.length, endIdx)
|
|
59
|
+
.trim();
|
|
60
|
+
state.parseBuffer = state.parseBuffer.slice(endIdx + OUTPUT_END_MARKER.length);
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(jsonStr);
|
|
63
|
+
if (parsed.newSessionId) {
|
|
64
|
+
state.newSessionId = parsed.newSessionId;
|
|
65
|
+
}
|
|
66
|
+
if (parsed.status === 'success') {
|
|
67
|
+
state.hasSuccessOutput = true;
|
|
68
|
+
}
|
|
69
|
+
if (parsed.status === 'error') {
|
|
70
|
+
state.lastErrorOutput = parsed;
|
|
71
|
+
}
|
|
72
|
+
if (parsed.status === 'closed') {
|
|
73
|
+
state.hasClosedOutput = true;
|
|
74
|
+
}
|
|
75
|
+
if (parsed.status === 'stream' &&
|
|
76
|
+
parsed.streamEvent?.statusText === 'interrupted') {
|
|
77
|
+
state.hasInterruptedOutput = true;
|
|
78
|
+
}
|
|
79
|
+
// Activity detected — reset the hard timeout
|
|
80
|
+
opts.resetTimeout();
|
|
81
|
+
// Call onOutput for all markers (including null results)
|
|
82
|
+
// so idle timers start even for "silent" query completions.
|
|
83
|
+
const onOutputFn = opts.onOutput;
|
|
84
|
+
state.outputChain = state.outputChain
|
|
85
|
+
.then(() => onOutputFn(parsed))
|
|
86
|
+
.catch((err) => {
|
|
87
|
+
logger.error({ group: opts.groupName, err }, 'onOutput callback error');
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
logger.warn({ group: opts.groupName, error: err }, 'Failed to parse streamed output chunk');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function createStderrState() {
|
|
98
|
+
return {
|
|
99
|
+
stderr: '',
|
|
100
|
+
stderrTruncated: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function attachStderrHandler(stream, state, groupName,
|
|
104
|
+
/** Log context key: { container: folder } or { host: folder } */
|
|
105
|
+
logContext) {
|
|
106
|
+
stream.on('data', (data) => {
|
|
107
|
+
const chunk = data.toString();
|
|
108
|
+
const lines = chunk.trim().split('\n');
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
if (line)
|
|
111
|
+
logger.debug(logContext, line);
|
|
112
|
+
}
|
|
113
|
+
// Don't reset timeout on stderr — SDK writes debug logs continuously.
|
|
114
|
+
// Timeout only resets on actual output (OUTPUT_MARKER in stdout).
|
|
115
|
+
if (state.stderrTruncated)
|
|
116
|
+
return;
|
|
117
|
+
const remaining = getSystemSettings().containerMaxOutputSize - state.stderr.length;
|
|
118
|
+
if (chunk.length > remaining) {
|
|
119
|
+
state.stderr += chunk.slice(0, remaining);
|
|
120
|
+
state.stderrTruncated = true;
|
|
121
|
+
logger.warn({ group: groupName, size: state.stderr.length }, `${Object.keys(logContext)[0] === 'container' ? 'Container' : 'Host agent'} stderr truncated due to size limit`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
state.stderr += chunk;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Handle the 'close' event for timeout case.
|
|
130
|
+
* Returns true if this was a timeout (caller should return early).
|
|
131
|
+
*/
|
|
132
|
+
export function handleTimeoutClose(ctx, code, duration, timedOut) {
|
|
133
|
+
if (!timedOut)
|
|
134
|
+
return false;
|
|
135
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
136
|
+
fs.mkdirSync(ctx.logsDir, { recursive: true });
|
|
137
|
+
const timeoutLog = path.join(ctx.logsDir, `${ctx.filePrefix}-${ts}.log`);
|
|
138
|
+
fs.writeFileSync(timeoutLog, [
|
|
139
|
+
`=== ${ctx.label} Run Log (TIMEOUT) ===`,
|
|
140
|
+
`Timestamp: ${new Date().toISOString()}`,
|
|
141
|
+
`Group: ${ctx.groupName}`,
|
|
142
|
+
`${ctx.label === 'Container' ? 'Container' : 'Process ID'}: ${ctx.identifier}`,
|
|
143
|
+
`Duration: ${duration}ms`,
|
|
144
|
+
`Exit Code: ${code}`,
|
|
145
|
+
].join('\n'));
|
|
146
|
+
logger.error({
|
|
147
|
+
group: ctx.groupName,
|
|
148
|
+
[ctx.filePrefix === 'container' ? 'containerName' : 'processId']: ctx.identifier,
|
|
149
|
+
duration,
|
|
150
|
+
code,
|
|
151
|
+
}, `${ctx.label} timed out`);
|
|
152
|
+
ctx.resolvePromise({
|
|
153
|
+
status: 'error',
|
|
154
|
+
result: null,
|
|
155
|
+
error: `${ctx.label} timed out after ${ctx.timeoutMs}ms`,
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Write a run log file. Returns the log file path.
|
|
161
|
+
*/
|
|
162
|
+
export function writeRunLog(ctx, code, duration) {
|
|
163
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
164
|
+
fs.mkdirSync(ctx.logsDir, { recursive: true });
|
|
165
|
+
const logFile = path.join(ctx.logsDir, `${ctx.filePrefix}-${timestamp}.log`);
|
|
166
|
+
const isVerbose = process.env.LOG_LEVEL === 'debug' || process.env.LOG_LEVEL === 'trace';
|
|
167
|
+
const logLines = [
|
|
168
|
+
`=== ${ctx.label} Run Log ===`,
|
|
169
|
+
`Timestamp: ${new Date().toISOString()}`,
|
|
170
|
+
`Group: ${ctx.groupName}`,
|
|
171
|
+
`IsMain: ${ctx.input.isMain}`,
|
|
172
|
+
`Duration: ${duration}ms`,
|
|
173
|
+
`Exit Code: ${code}`,
|
|
174
|
+
`Stdout Truncated: ${ctx.stdoutState.stdoutTruncated}`,
|
|
175
|
+
`Stderr Truncated: ${ctx.stderrState.stderrTruncated}`,
|
|
176
|
+
``,
|
|
177
|
+
];
|
|
178
|
+
const isError = code !== 0;
|
|
179
|
+
const { stderr, stderrTruncated } = ctx.stderrState;
|
|
180
|
+
const { stdout, stdoutTruncated } = ctx.stdoutState;
|
|
181
|
+
const LOG_TAIL_LIMIT = 4000;
|
|
182
|
+
const stderrLog = !isVerbose && !isError && stderr.length > LOG_TAIL_LIMIT
|
|
183
|
+
? `... (truncated ${stderr.length - LOG_TAIL_LIMIT} chars) ...\n` +
|
|
184
|
+
stderr.slice(-LOG_TAIL_LIMIT)
|
|
185
|
+
: stderr;
|
|
186
|
+
const stdoutLog = !isVerbose && !isError && stdout.length > LOG_TAIL_LIMIT
|
|
187
|
+
? `... (truncated ${stdout.length - LOG_TAIL_LIMIT} chars) ...\n` +
|
|
188
|
+
stdout.slice(-LOG_TAIL_LIMIT)
|
|
189
|
+
: stdout;
|
|
190
|
+
logLines.push(`=== Input Summary ===`, `Prompt length: ${ctx.input.prompt.length} chars`, `Session ID: ${ctx.input.sessionId || 'new'}`);
|
|
191
|
+
if (ctx.agentIdentity?.chatJid) {
|
|
192
|
+
logLines.push(`Chat JID: ${ctx.agentIdentity.chatJid}`);
|
|
193
|
+
}
|
|
194
|
+
if (ctx.agentIdentity?.groupFolder) {
|
|
195
|
+
logLines.push(`Group Folder: ${ctx.agentIdentity.groupFolder}`);
|
|
196
|
+
}
|
|
197
|
+
if (ctx.agentIdentity?.agentType) {
|
|
198
|
+
logLines.push(`Agent Type: ${ctx.agentIdentity.agentType}`);
|
|
199
|
+
}
|
|
200
|
+
if (ctx.agentIdentity?.executionMode) {
|
|
201
|
+
logLines.push(`Execution Mode: ${ctx.agentIdentity.executionMode}`);
|
|
202
|
+
}
|
|
203
|
+
if (ctx.agentIdentity?.selectedRunner) {
|
|
204
|
+
logLines.push(`Selected Runner: ${ctx.agentIdentity.selectedRunner}`);
|
|
205
|
+
}
|
|
206
|
+
if (ctx.agentIdentity?.agentId) {
|
|
207
|
+
logLines.push(`Agent ID: ${ctx.agentIdentity.agentId}`);
|
|
208
|
+
}
|
|
209
|
+
if (ctx.runtimeBuildInfo) {
|
|
210
|
+
logLines.push(`Backend PID: ${ctx.runtimeBuildInfo.backendPid}`, `Backend Started At: ${ctx.runtimeBuildInfo.backendStartedAt}`, `Backend Build Loaded: ${ctx.runtimeBuildInfo.backendBuildLoaded}`, `Backend Build Current: ${ctx.runtimeBuildInfo.backendBuildCurrent}`, `Backend Build Stale: ${ctx.runtimeBuildInfo.backendBuildStale}`, `Agent Runner Build Loaded: ${ctx.runtimeBuildInfo.agentRunnerBuildLoaded}`, `Agent Runner Build Current: ${ctx.runtimeBuildInfo.agentRunnerBuildCurrent}`, `Agent Runner Build Stale: ${ctx.runtimeBuildInfo.agentRunnerBuildStale}`);
|
|
211
|
+
}
|
|
212
|
+
if (ctx.extraSummaryLines) {
|
|
213
|
+
logLines.push(...ctx.extraSummaryLines);
|
|
214
|
+
}
|
|
215
|
+
logLines.push(``, `=== Stderr${stderrTruncated ? ' (TRUNCATED)' : ''} ===`, stderrLog, ``, `=== Stdout${stdoutTruncated ? ' (TRUNCATED)' : ''} ===`, stdoutLog);
|
|
216
|
+
if (isVerbose || isError) {
|
|
217
|
+
logLines.push(``, `=== Input ===`, JSON.stringify(ctx.input, null, 2));
|
|
218
|
+
if (ctx.extraVerboseLines) {
|
|
219
|
+
logLines.push(``, ...ctx.extraVerboseLines);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
fs.writeFileSync(logFile, logLines.join('\n'));
|
|
223
|
+
logger.debug({ logFile, verbose: isVerbose }, `${ctx.label} log written`);
|
|
224
|
+
return logFile;
|
|
225
|
+
}
|
|
226
|
+
const OUTPUT_CHAIN_TIMEOUT = 30_000;
|
|
227
|
+
/**
|
|
228
|
+
* Wait for the output chain to settle with a safety timeout.
|
|
229
|
+
* Calls `then` callback on success, always ensures chain timer is cleaned up.
|
|
230
|
+
*/
|
|
231
|
+
function waitForOutputChain(outputChain, groupName, logLabel, then) {
|
|
232
|
+
let chainTimer = null;
|
|
233
|
+
const chainTimeout = new Promise((resolve) => {
|
|
234
|
+
chainTimer = setTimeout(() => {
|
|
235
|
+
logger.warn({ group: groupName, timeoutMs: OUTPUT_CHAIN_TIMEOUT }, `Output chain settle timeout on ${logLabel}`);
|
|
236
|
+
resolve();
|
|
237
|
+
}, OUTPUT_CHAIN_TIMEOUT);
|
|
238
|
+
});
|
|
239
|
+
Promise.race([outputChain, chainTimeout])
|
|
240
|
+
.then(() => {
|
|
241
|
+
if (chainTimer)
|
|
242
|
+
clearTimeout(chainTimer);
|
|
243
|
+
then();
|
|
244
|
+
})
|
|
245
|
+
.catch(() => {
|
|
246
|
+
if (chainTimer)
|
|
247
|
+
clearTimeout(chainTimer);
|
|
248
|
+
then();
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Handle the non-zero exit code path (force-kill detection, error output chain, resolve).
|
|
253
|
+
* Returns true if handled (caller should return early).
|
|
254
|
+
*/
|
|
255
|
+
export function handleNonZeroExit(ctx, code, signal, duration, logFile) {
|
|
256
|
+
if (code === 0)
|
|
257
|
+
return false;
|
|
258
|
+
const exitLabel = code === null ? `signal ${signal || 'unknown'}` : `code ${code}`;
|
|
259
|
+
const { newSessionId, outputChain } = ctx.stdoutState;
|
|
260
|
+
// Graceful interrupt: agent emitted 'interrupted' status before exiting.
|
|
261
|
+
if (ctx.stdoutState.hasInterruptedOutput && ctx.onOutput) {
|
|
262
|
+
logger.info({ group: ctx.groupName, code, signal, duration, newSessionId }, `${ctx.label} exited after interrupt (treating as success)`);
|
|
263
|
+
waitForOutputChain(outputChain, ctx.groupName, `${ctx.filePrefix} interrupt path`, () => {
|
|
264
|
+
ctx.resolvePromise({ status: 'success', result: null, newSessionId });
|
|
265
|
+
});
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
// Graceful shutdown: agent was killed by SIGTERM/SIGKILL (e.g. user
|
|
269
|
+
// clicked stop, session reset, clear-history). Treat as normal
|
|
270
|
+
// completion instead of an error — BUT only if the agent had already
|
|
271
|
+
// produced some output. If killed before emitting ANY output markers
|
|
272
|
+
// (success/closed), it means the process died during initialization
|
|
273
|
+
// (e.g., race condition) and should be treated as an error so the UI
|
|
274
|
+
// waiting state gets cleared via sendSystemMessage('agent_error').
|
|
275
|
+
const isForceKilled = signal === 'SIGTERM' || signal === 'SIGKILL' || code === 137;
|
|
276
|
+
if (isForceKilled && ctx.onOutput) {
|
|
277
|
+
const hadOutput = ctx.stdoutState.hasSuccessOutput || ctx.stdoutState.hasClosedOutput;
|
|
278
|
+
if (hadOutput) {
|
|
279
|
+
logger.info({ group: ctx.groupName, signal, code, duration, newSessionId }, `${ctx.label} terminated by signal (user stop / graceful shutdown)`);
|
|
280
|
+
waitForOutputChain(outputChain, ctx.groupName, `${ctx.filePrefix} force-kill path`, () => {
|
|
281
|
+
ctx.resolvePromise({
|
|
282
|
+
status: 'success',
|
|
283
|
+
result: null,
|
|
284
|
+
newSessionId,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
// Agent was killed before producing any output — fall through to
|
|
290
|
+
// error path so the caller can broadcast an error and clear the UI.
|
|
291
|
+
logger.warn({ group: ctx.groupName, signal, code, duration }, `${ctx.label} killed before producing any output — treating as error`);
|
|
292
|
+
}
|
|
293
|
+
// Build error output
|
|
294
|
+
const { stderr } = ctx.stderrState;
|
|
295
|
+
const enriched = ctx.enrichError
|
|
296
|
+
? ctx.enrichError(stderr, exitLabel)
|
|
297
|
+
: {
|
|
298
|
+
result: null,
|
|
299
|
+
error: `${ctx.label} exited with ${exitLabel}: ${stderr.slice(-200)}`,
|
|
300
|
+
};
|
|
301
|
+
logger.error({
|
|
302
|
+
group: ctx.groupName,
|
|
303
|
+
code,
|
|
304
|
+
signal,
|
|
305
|
+
duration,
|
|
306
|
+
stderr,
|
|
307
|
+
stdout: ctx.stdoutState.stdout,
|
|
308
|
+
logFile,
|
|
309
|
+
}, `${ctx.label} exited with error`);
|
|
310
|
+
const finalizeError = () => {
|
|
311
|
+
if (ctx.stdoutState.lastErrorOutput) {
|
|
312
|
+
const streamedError = ctx.stdoutState.lastErrorOutput;
|
|
313
|
+
ctx.resolvePromise({
|
|
314
|
+
...streamedError,
|
|
315
|
+
result: streamedError.result ?? enriched.result,
|
|
316
|
+
error: streamedError.error || enriched.error,
|
|
317
|
+
finalizationReason: streamedError.finalizationReason || 'error',
|
|
318
|
+
alreadyStreamedError: true,
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
ctx.resolvePromise({
|
|
323
|
+
status: 'error',
|
|
324
|
+
result: enriched.result,
|
|
325
|
+
error: enriched.error,
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
// Even on error exits, wait for pending output callbacks to settle.
|
|
329
|
+
if (ctx.onOutput) {
|
|
330
|
+
waitForOutputChain(outputChain, ctx.groupName, `${ctx.filePrefix} error path`, finalizeError);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
finalizeError();
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Handle the success (code === 0) path — streaming mode or legacy parsing.
|
|
338
|
+
*/
|
|
339
|
+
export function handleSuccessClose(ctx, duration) {
|
|
340
|
+
const { newSessionId, outputChain } = ctx.stdoutState;
|
|
341
|
+
// Streaming mode: wait for output chain to settle
|
|
342
|
+
if (ctx.onOutput) {
|
|
343
|
+
const { hasClosedOutput } = ctx.stdoutState;
|
|
344
|
+
waitForOutputChain(outputChain, ctx.groupName, `${ctx.filePrefix} success path`, () => {
|
|
345
|
+
// Propagate 'closed' status so the host can distinguish a _close-interrupted
|
|
346
|
+
// exit from a normal completion and avoid committing the message cursor.
|
|
347
|
+
const finalStatus = hasClosedOutput
|
|
348
|
+
? 'closed'
|
|
349
|
+
: 'success';
|
|
350
|
+
logger.info({ group: ctx.groupName, duration, newSessionId, finalStatus }, `${ctx.label} completed (streaming mode)`);
|
|
351
|
+
ctx.resolvePromise({
|
|
352
|
+
status: finalStatus,
|
|
353
|
+
result: null,
|
|
354
|
+
newSessionId,
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// Legacy mode: parse the last output marker pair from accumulated stdout
|
|
360
|
+
parseLegacyOutput(ctx);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Parse legacy (non-streaming) output from accumulated stdout.
|
|
364
|
+
*/
|
|
365
|
+
function parseLegacyOutput(ctx) {
|
|
366
|
+
const { stdout } = ctx.stdoutState;
|
|
367
|
+
try {
|
|
368
|
+
const startIdx = stdout.indexOf(OUTPUT_START_MARKER);
|
|
369
|
+
const endIdx = stdout.indexOf(OUTPUT_END_MARKER);
|
|
370
|
+
let jsonLine;
|
|
371
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
372
|
+
jsonLine = stdout
|
|
373
|
+
.slice(startIdx + OUTPUT_START_MARKER.length, endIdx)
|
|
374
|
+
.trim();
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// Fallback: last non-empty line (backwards compatibility)
|
|
378
|
+
const lines = stdout.trim().split('\n');
|
|
379
|
+
jsonLine = lines[lines.length - 1];
|
|
380
|
+
}
|
|
381
|
+
const output = JSON.parse(jsonLine);
|
|
382
|
+
logger.info({
|
|
383
|
+
group: ctx.groupName,
|
|
384
|
+
duration: Date.now() - ctx.startTime,
|
|
385
|
+
status: output.status,
|
|
386
|
+
hasResult: !!output.result,
|
|
387
|
+
}, `${ctx.label} completed`);
|
|
388
|
+
ctx.resolvePromise(output);
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
logger.error({
|
|
392
|
+
group: ctx.groupName,
|
|
393
|
+
stdout,
|
|
394
|
+
stderr: ctx.stderrState.stderr,
|
|
395
|
+
error: err,
|
|
396
|
+
}, `Failed to parse ${ctx.filePrefix} output`);
|
|
397
|
+
ctx.resolvePromise({
|
|
398
|
+
status: 'error',
|
|
399
|
+
result: null,
|
|
400
|
+
error: `Failed to parse ${ctx.filePrefix} output: ${err instanceof Error ? err.message : String(err)}`,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// ─── API Error Classification ────────────────────────────────────────
|
|
405
|
+
function normalizeRuntimeErrorText(stderr) {
|
|
406
|
+
return stderr
|
|
407
|
+
.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ' ')
|
|
408
|
+
.replace(/\s+/g, ' ')
|
|
409
|
+
.trim();
|
|
410
|
+
}
|
|
411
|
+
export function formatUserFacingRuntimeError(stderr) {
|
|
412
|
+
const normalized = normalizeRuntimeErrorText(stderr);
|
|
413
|
+
if (!normalized)
|
|
414
|
+
return null;
|
|
415
|
+
if (/Codex CLI 未登录/u.test(normalized) ||
|
|
416
|
+
/auth_required|login required|please login|not logged in/i.test(normalized)) {
|
|
417
|
+
return 'Codex CLI 未登录。请先在服务器上执行:codex login';
|
|
418
|
+
}
|
|
419
|
+
if (/UsageLimitExceeded/i.test(normalized) ||
|
|
420
|
+
/purchase more credits/i.test(normalized) ||
|
|
421
|
+
/https:\/\/chatgpt\.com\/codex\/settings\/usage/i.test(normalized)) {
|
|
422
|
+
const usageUrl = normalized.match(/https:\/\/chatgpt\.com\/codex\/settings\/usage/i)?.[0] || 'https://chatgpt.com/codex/settings/usage';
|
|
423
|
+
const retryAt = normalized.match(/try again at ([^.]+)\.?/i)?.[1]?.trim();
|
|
424
|
+
return retryAt
|
|
425
|
+
? `Codex CLI 用量已用尽。请前往 ${usageUrl} 购买额度,或在 ${retryAt} 后重试。`
|
|
426
|
+
: `Codex CLI 用量已用尽。请前往 ${usageUrl} 购买额度,或稍后重试。`;
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
/** Patterns that indicate an API-level error (provider issue, not user code bug) */
|
|
431
|
+
const API_ERROR_PATTERNS = [
|
|
432
|
+
/\bapi[_ ]?key\b.*\b(invalid|missing|expired|required)\b/i,
|
|
433
|
+
/\bauthentication\s+(failed|error|required)\b/i,
|
|
434
|
+
/\b(401|403)\b.*\bunauthorized\b/i,
|
|
435
|
+
/\brate[_ ]?limit(ed)?\b/i,
|
|
436
|
+
/\bquota\s+(exceeded|exhausted)\b/i,
|
|
437
|
+
/\boverloaded\b/i,
|
|
438
|
+
/\binternal\s+server\s+error\b/i,
|
|
439
|
+
/\b(502|503|504|529)\b/,
|
|
440
|
+
/ANTHROPIC_API_KEY/,
|
|
441
|
+
/ANTHROPIC_AUTH_TOKEN/,
|
|
442
|
+
/\binvalid[_ ]?api\b/i,
|
|
443
|
+
/\bbilling\s+(error|issue|limit)\b/i,
|
|
444
|
+
/\bcredit(s)?\s+(exhausted|insufficient)\b/i,
|
|
445
|
+
/connection\s*(refused|reset|timed?\s*out)/i,
|
|
446
|
+
/ECONNREFUSED|ECONNRESET|ETIMEDOUT/,
|
|
447
|
+
];
|
|
448
|
+
/**
|
|
449
|
+
* Classify whether stderr output indicates an API-level error
|
|
450
|
+
* (provider unreachable, auth failure, rate limit, etc.)
|
|
451
|
+
* vs a normal agent exit or user code issue.
|
|
452
|
+
*
|
|
453
|
+
* Used by container-runner to decide whether to report failure to ProviderPool.
|
|
454
|
+
*/
|
|
455
|
+
export function isApiError(stderr) {
|
|
456
|
+
if (!stderr)
|
|
457
|
+
return false;
|
|
458
|
+
return API_ERROR_PATTERNS.some((pattern) => pattern.test(stderr));
|
|
459
|
+
}
|
package/dist/app-root.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
function toModuleDirectory(moduleLocation) {
|
|
6
|
+
const resolvedLocation = moduleLocation.startsWith('file:')
|
|
7
|
+
? fileURLToPath(moduleLocation)
|
|
8
|
+
: path.resolve(moduleLocation);
|
|
9
|
+
try {
|
|
10
|
+
if (fs.statSync(resolvedLocation).isDirectory()) {
|
|
11
|
+
return resolvedLocation;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Fall back to treating the input as a file path.
|
|
16
|
+
}
|
|
17
|
+
return path.dirname(resolvedLocation);
|
|
18
|
+
}
|
|
19
|
+
export function findPackageRoot(moduleLocation) {
|
|
20
|
+
let currentDir = toModuleDirectory(moduleLocation);
|
|
21
|
+
while (true) {
|
|
22
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
23
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
24
|
+
return currentDir;
|
|
25
|
+
}
|
|
26
|
+
const parentDir = path.dirname(currentDir);
|
|
27
|
+
if (parentDir === currentDir) {
|
|
28
|
+
throw new Error(`Unable to locate package.json from ${moduleLocation}`);
|
|
29
|
+
}
|
|
30
|
+
currentDir = parentDir;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export const PACKAGE_ROOT = findPackageRoot(import.meta.url);
|
|
34
|
+
export const APP_ROOT = PACKAGE_ROOT;
|
|
35
|
+
export const LAUNCH_CWD = (() => {
|
|
36
|
+
try {
|
|
37
|
+
return fs.realpathSync(process.cwd());
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return path.resolve(process.cwd());
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
export function resolveAppPath(...segments) {
|
|
44
|
+
return path.join(APP_ROOT, ...segments);
|
|
45
|
+
}
|
|
46
|
+
export function resolvePackagePath(...segments) {
|
|
47
|
+
return path.join(PACKAGE_ROOT, ...segments);
|
|
48
|
+
}
|
|
49
|
+
export function resolvePackageDependency(specifier) {
|
|
50
|
+
const packageRequire = createRequire(resolvePackagePath('package.json'));
|
|
51
|
+
return packageRequire.resolve(specifier);
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { formatAssistantMetaFooter, formatCompactNumber, getAssistantMetaFooterParts, parseAssistantTokenUsage, } from '../shared/dist/assistant-meta-footer.js';
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import bcrypt from 'bcryptjs';
|
|
3
|
+
const BCRYPT_ROUNDS = 12;
|
|
4
|
+
// --- Password hashing ---
|
|
5
|
+
export async function hashPassword(password) {
|
|
6
|
+
return bcrypt.hash(password, BCRYPT_ROUNDS);
|
|
7
|
+
}
|
|
8
|
+
export async function verifyPassword(password, hash) {
|
|
9
|
+
return bcrypt.compare(password, hash);
|
|
10
|
+
}
|
|
11
|
+
// --- Session token generation ---
|
|
12
|
+
export function generateSessionToken() {
|
|
13
|
+
return crypto.randomBytes(32).toString('hex');
|
|
14
|
+
}
|
|
15
|
+
export function generateUserId() {
|
|
16
|
+
return crypto.randomUUID();
|
|
17
|
+
}
|
|
18
|
+
export function generateInviteCode() {
|
|
19
|
+
return crypto.randomBytes(16).toString('hex');
|
|
20
|
+
}
|
|
21
|
+
// --- Input validation ---
|
|
22
|
+
const USERNAME_RE = /^[a-zA-Z0-9_]{3,32}$/;
|
|
23
|
+
const PASSWORD_MIN = 8;
|
|
24
|
+
const PASSWORD_MAX = 128;
|
|
25
|
+
export function validateUsername(username) {
|
|
26
|
+
if (!username || typeof username !== 'string')
|
|
27
|
+
return '用户名不能为空';
|
|
28
|
+
if (!USERNAME_RE.test(username))
|
|
29
|
+
return '用户名须为3-32位字母、数字或下划线';
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
export function validatePassword(password) {
|
|
33
|
+
if (!password || typeof password !== 'string')
|
|
34
|
+
return '密码不能为空';
|
|
35
|
+
if (password.length < PASSWORD_MIN)
|
|
36
|
+
return `密码长度不能少于${PASSWORD_MIN}位`;
|
|
37
|
+
if (password.length > PASSWORD_MAX)
|
|
38
|
+
return `密码长度不能超过${PASSWORD_MAX}位`;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const loginAttempts = new Map();
|
|
42
|
+
// Sliding window: clean old entries every 10 minutes
|
|
43
|
+
setInterval(() => {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
for (const [key, record] of loginAttempts) {
|
|
46
|
+
// Remove entries older than lockout period * 2
|
|
47
|
+
if (now - record.lastAttempt > 30 * 60 * 1000) {
|
|
48
|
+
loginAttempts.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, 10 * 60 * 1000);
|
|
52
|
+
export function checkLoginRateLimit(username, ip, maxAttempts, lockoutMinutes) {
|
|
53
|
+
const key = `${username}:${ip}`;
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const windowMs = lockoutMinutes * 60 * 1000;
|
|
56
|
+
const record = loginAttempts.get(key);
|
|
57
|
+
if (!record)
|
|
58
|
+
return { allowed: true };
|
|
59
|
+
// Reset if window has passed since first attempt
|
|
60
|
+
if (now - record.firstAttempt > windowMs) {
|
|
61
|
+
loginAttempts.delete(key);
|
|
62
|
+
return { allowed: true };
|
|
63
|
+
}
|
|
64
|
+
if (record.count >= maxAttempts) {
|
|
65
|
+
const retryAfter = Math.ceil((record.firstAttempt + windowMs - now) / 1000);
|
|
66
|
+
return { allowed: false, retryAfterSeconds: Math.max(1, retryAfter) };
|
|
67
|
+
}
|
|
68
|
+
return { allowed: true };
|
|
69
|
+
}
|
|
70
|
+
export function recordLoginAttempt(username, ip) {
|
|
71
|
+
const key = `${username}:${ip}`;
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const record = loginAttempts.get(key);
|
|
74
|
+
if (record) {
|
|
75
|
+
record.count += 1;
|
|
76
|
+
record.lastAttempt = now;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
loginAttempts.set(key, { count: 1, firstAttempt: now, lastAttempt: now });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function clearLoginAttempts(username, ip) {
|
|
83
|
+
loginAttempts.delete(`${username}:${ip}`);
|
|
84
|
+
}
|
|
85
|
+
// --- Session expiry ---
|
|
86
|
+
export function sessionExpiresAt() {
|
|
87
|
+
return new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
88
|
+
}
|
|
89
|
+
export function isSessionExpired(expiresAt) {
|
|
90
|
+
return new Date(expiresAt).getTime() < Date.now();
|
|
91
|
+
}
|