@geminilight/mindos 0.6.73 → 0.6.75
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/_standalone/.mindos-build-version +1 -1
- package/_standalone/.next/BUILD_ID +1 -1
- package/_standalone/.next/app-path-routes-manifest.json +30 -30
- package/_standalone/.next/build-manifest.json +3 -3
- package/_standalone/.next/cache/.previewinfo +1 -1
- package/_standalone/.next/cache/.rscinfo +1 -1
- package/_standalone/.next/cache/config.json +3 -3
- package/_standalone/.next/prerender-manifest.json +3 -3
- package/_standalone/.next/react-loadable-manifest.json +5 -5
- package/_standalone/.next/required-server-files.json +14 -1
- package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/_global-error.html +2 -2
- package/_standalone/.next/server/app/_global-error.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/_standalone/.next/server/app/_not-found/page.js +1 -1
- package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/agents/page.js +1 -1
- package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/copy-skill/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask/route.js +1 -1
- package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/channels/verify/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/channels/verify/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/connect/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/embedding/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/embedding/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/extract-pdf/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/raw/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/activity/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/config/route.js +1 -1
- package/_standalone/.next/server/app/api/im/config/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/status/route.js +1 -7
- package/_standalone/.next/server/app/api/im/status/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/im/status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/test/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/im/test/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/webhook/feishu/route.js +1 -7
- package/_standalone/.next/server/app/api/im/webhook/feishu/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/im/webhook/feishu/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/im/webhook-status/route.js +1 -7
- package/_standalone/.next/server/app/api/im/webhook-status/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/im/webhook-status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/inbox/clip/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/inbox/clip/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/lint/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/lint/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/direct-tools/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/tools/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/space-overview/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/space-overview/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
- package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/changelog/page.js +1 -1
- package/_standalone/.next/server/app/changelog/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/changes/page.js +1 -1
- package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/echo/page.js +1 -1
- package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/explore/page.js +1 -1
- package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/help/page.js +1 -1
- package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js +1 -1
- package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/login/page.js +1 -1
- package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/page.js +1 -1
- package/_standalone/.next/server/app/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/setup/page.js +2 -2
- package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/trash/page.js +3 -3
- package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app/view/[...path]/page.js +3 -3
- package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
- package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
- package/_standalone/.next/server/app-paths-manifest.json +30 -30
- package/_standalone/.next/server/chunks/1076.js +1 -1
- package/_standalone/.next/server/chunks/{2449.js → 2424.js} +2 -2
- package/_standalone/.next/server/chunks/2792.js +1 -0
- package/_standalone/.next/server/chunks/2885.js +7 -0
- package/_standalone/.next/server/chunks/3800.js +1 -1
- package/_standalone/.next/server/chunks/5299.js +1 -1
- package/_standalone/.next/server/chunks/5464.js +1 -1
- package/_standalone/.next/server/chunks/6022.js +28 -28
- package/_standalone/.next/server/chunks/6539.js +1 -1
- package/_standalone/.next/server/chunks/8388.js +2 -2
- package/_standalone/.next/server/middleware-build-manifest.js +1 -1
- package/_standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/_standalone/.next/server/pages/500.html +2 -2
- package/_standalone/.next/server/server-reference-manifest.js +1 -1
- package/_standalone/.next/server/server-reference-manifest.json +1 -1
- package/_standalone/.next/static/chunks/{5331.c89084fd7f67887d.js → 4342.ccc18d7a45f204a4.js} +5 -5
- package/_standalone/.next/static/chunks/{4094.09364c01df411380.js → 4496.f77a677ac2034e48.js} +1 -1
- package/_standalone/.next/static/chunks/{5358-15618fc9b20018ee.js → 5358-15ceeb8c1cd9889d.js} +6 -6
- package/_standalone/.next/static/chunks/{6902-edc5c487c696bd0b.js → 6902-53a31f849968ddf8.js} +1 -1
- package/_standalone/.next/static/chunks/9207-456dc6941d557f33.js +1 -0
- package/_standalone/.next/static/chunks/app/{layout-fcbde5bee626d21a.js → layout-09f08f52e3cbfc47.js} +66 -66
- package/_standalone/.next/static/chunks/app/setup/{page-e40f3c5704c3c857.js → page-64d7fa097ef3f367.js} +1 -1
- package/_standalone/.next/static/chunks/app/trash/page-8e0ea3ba71702757.js +1 -0
- package/_standalone/.next/static/chunks/app/view/[...path]/page-04f43178679a8bfb.js +12 -0
- package/_standalone/.next/static/chunks/{webpack-dc486b68118d1328.js → webpack-ec9c6076012b75f3.js} +1 -1
- package/_standalone/.next/trace +72 -72
- package/_standalone/__tests__/agent/provider-presets.test.ts +32 -0
- package/_standalone/__tests__/api/im-config-feishu-conversation.test.ts +39 -0
- package/_standalone/__tests__/api/im-webhook-feishu.test.ts +21 -12
- package/_standalone/__tests__/api/pi-subagents.test.ts +81 -0
- package/_standalone/__tests__/ask/file-chip-variants.test.tsx +18 -0
- package/_standalone/__tests__/ask/message-list-agent-attribution.test.tsx +2 -0
- package/_standalone/__tests__/im/feishu-dispatcher.test.ts +151 -0
- package/_standalone/__tests__/im/feishu-webhook.test.ts +85 -57
- package/_standalone/__tests__/im/feishu-ws-client.test.ts +91 -0
- package/_standalone/__tests__/settings/custom-provider-form.test.ts +30 -0
- package/_standalone/components/ask/FileChip.tsx +1 -1
- package/_standalone/components/ask/MessageList.tsx +2 -2
- package/_standalone/components/settings/CustomProviderFields.tsx +1 -1
- package/_standalone/components/settings/CustomProvidersCard.tsx +3 -0
- package/_standalone/components/settings/useCustomProviderForm.ts +78 -24
- package/_standalone/components/shared/ProviderSelect.tsx +6 -5
- package/_standalone/next.config.ts +28 -4
- package/_standalone/package-lock.json +20 -2
- package/_standalone/package.json +3 -1
- package/_standalone/scripts/feishu-long-connection.ts +32 -0
- package/_standalone/server.js +1 -1
- package/_standalone/tsconfig.tsbuildinfo +1 -1
- package/app/__tests__/agent/provider-presets.test.ts +32 -0
- package/app/__tests__/api/im-config-feishu-conversation.test.ts +39 -0
- package/app/__tests__/api/im-webhook-feishu.test.ts +21 -12
- package/app/__tests__/api/pi-subagents.test.ts +81 -0
- package/app/__tests__/ask/file-chip-variants.test.tsx +18 -0
- package/app/__tests__/ask/message-list-agent-attribution.test.tsx +2 -0
- package/app/__tests__/im/feishu-dispatcher.test.ts +151 -0
- package/app/__tests__/im/feishu-webhook.test.ts +85 -57
- package/app/__tests__/im/feishu-ws-client.test.ts +91 -0
- package/app/__tests__/settings/custom-provider-form.test.ts +30 -0
- package/app/app/api/ask/route.ts +2 -0
- package/app/app/api/im/config/route.ts +2 -0
- package/app/app/api/im/webhook/feishu/route.ts +4 -2
- package/app/components/ask/FileChip.tsx +1 -1
- package/app/components/ask/MessageList.tsx +2 -2
- package/app/components/settings/CustomProviderFields.tsx +1 -1
- package/app/components/settings/CustomProvidersCard.tsx +3 -0
- package/app/components/settings/useCustomProviderForm.ts +78 -24
- package/app/components/shared/ProviderSelect.tsx +6 -5
- package/app/lib/agent/providers.ts +44 -6
- package/app/lib/im/feishu-dispatcher.ts +111 -0
- package/app/lib/im/feishu-ws-client.ts +72 -0
- package/app/lib/im/types.ts +14 -0
- package/app/lib/im/webhook/feishu.ts +44 -55
- package/app/next.config.ts +28 -4
- package/app/package.json +3 -1
- package/app/scripts/feishu-long-connection.ts +32 -0
- package/bin/cli.js +3 -1
- package/bin/commands/feishu-ws.js +39 -0
- package/package.json +1 -1
- package/scripts/build-runtime-archive.sh +36 -2
- package/_standalone/.next/static/chunks/9207-3b19c55a3c974a09.js +0 -1
- package/_standalone/.next/static/chunks/app/trash/page-e623ff0ab35de002.js +0 -1
- package/_standalone/.next/static/chunks/app/view/[...path]/page-49c4eff6ffdb5168.js +0 -12
- /package/_standalone/.next/static/{Dn8EHqUedSzanCfrM8WWS → dZDCx13MSM8QVQk2QNRs8}/_buildManifest.js +0 -0
- /package/_standalone/.next/static/{Dn8EHqUedSzanCfrM8WWS → dZDCx13MSM8QVQk2QNRs8}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { FeishuConfig } from '@/lib/im/types';
|
|
3
|
+
|
|
4
|
+
const { startMock, closeMock, wsClientCtor, registerMock } = vi.hoisted(() => {
|
|
5
|
+
const startMock = vi.fn().mockResolvedValue(undefined);
|
|
6
|
+
const closeMock = vi.fn();
|
|
7
|
+
const wsClientCtor = vi.fn();
|
|
8
|
+
const registerMock = vi.fn(function () { return this; });
|
|
9
|
+
return { startMock, closeMock, wsClientCtor, registerMock };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
vi.mock('@larksuiteoapi/node-sdk', () => ({
|
|
13
|
+
LoggerLevel: { info: 'info' },
|
|
14
|
+
EventDispatcher: class MockEventDispatcher {
|
|
15
|
+
register(handles: unknown) {
|
|
16
|
+
registerMock(handles);
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
WSClient: class MockWSClient {
|
|
21
|
+
constructor(params: unknown) {
|
|
22
|
+
wsClientCtor(params);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async start(params: unknown) {
|
|
26
|
+
return await startMock(params);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
close(params?: unknown) {
|
|
30
|
+
closeMock(params);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
describe('Feishu WS client manager', () => {
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
const mod = await import('@/lib/im/feishu-ws-client');
|
|
39
|
+
mod.__resetFeishuWSClientForTests();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('starts a WS client once for long connection', async () => {
|
|
43
|
+
const mod = await import('@/lib/im/feishu-ws-client');
|
|
44
|
+
const config: FeishuConfig = {
|
|
45
|
+
app_id: 'cli_xxx',
|
|
46
|
+
app_secret: 'secret',
|
|
47
|
+
conversation: { enabled: true, transport: 'long_connection' },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
await mod.startFeishuWSClient(config);
|
|
51
|
+
await mod.startFeishuWSClient(config);
|
|
52
|
+
|
|
53
|
+
expect(wsClientCtor).toHaveBeenCalledTimes(1);
|
|
54
|
+
expect(wsClientCtor).toHaveBeenCalledWith(expect.objectContaining({
|
|
55
|
+
appId: 'cli_xxx',
|
|
56
|
+
appSecret: 'secret',
|
|
57
|
+
autoReconnect: true,
|
|
58
|
+
}));
|
|
59
|
+
expect(startMock).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(registerMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
61
|
+
'im.message.receive_v1': expect.any(Function),
|
|
62
|
+
}));
|
|
63
|
+
expect(mod.getFeishuWSClientStatus().running).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('stops the running WS client', async () => {
|
|
67
|
+
const mod = await import('@/lib/im/feishu-ws-client');
|
|
68
|
+
await mod.startFeishuWSClient({
|
|
69
|
+
app_id: 'cli_xxx',
|
|
70
|
+
app_secret: 'secret',
|
|
71
|
+
conversation: { enabled: true, transport: 'long_connection' },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
mod.stopFeishuWSClient();
|
|
75
|
+
|
|
76
|
+
expect(closeMock).toHaveBeenCalled();
|
|
77
|
+
expect(mod.getFeishuWSClientStatus().running).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('reports configuration errors before trying to connect', async () => {
|
|
81
|
+
const mod = await import('@/lib/im/feishu-ws-client');
|
|
82
|
+
|
|
83
|
+
await expect(mod.startFeishuWSClient({
|
|
84
|
+
app_id: '',
|
|
85
|
+
app_secret: 'secret',
|
|
86
|
+
conversation: { enabled: true, transport: 'long_connection' },
|
|
87
|
+
})).rejects.toThrow('Feishu App ID and App Secret are required');
|
|
88
|
+
|
|
89
|
+
expect(wsClientCtor).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildDefaultProviderName } from '@/components/settings/useCustomProviderForm';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('buildDefaultProviderName', () => {
|
|
6
|
+
it('uses the protocol display name by default', () => {
|
|
7
|
+
expect(buildDefaultProviderName('openai', [], undefined, 'en')).toBe('OpenAI');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('appends numeric suffixes when the default name already exists', () => {
|
|
11
|
+
expect(buildDefaultProviderName('openai', ['OpenAI'], undefined, 'en')).toBe('OpenAI 2');
|
|
12
|
+
expect(buildDefaultProviderName('openai', ['OpenAI', 'OpenAI 2'], undefined, 'en')).toBe('OpenAI 3');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('ignores the currently edited provider name when computing duplicates', () => {
|
|
16
|
+
expect(buildDefaultProviderName('openai', ['OpenAI'], 'OpenAI', 'en')).toBe('OpenAI');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('uses localized protocol name in Chinese', () => {
|
|
20
|
+
expect(buildDefaultProviderName('minimax-cn', [], undefined, 'zh')).toBe('MiniMax (国内版)');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('uses LM Studio as the default generated name', () => {
|
|
24
|
+
expect(buildDefaultProviderName('lm-studio', [], undefined, 'en')).toBe('LM Studio');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uses vLLM as the default generated name', () => {
|
|
28
|
+
expect(buildDefaultProviderName('vllm', [], undefined, 'en')).toBe('vLLM');
|
|
29
|
+
});
|
|
30
|
+
});
|
package/app/app/api/ask/route.ts
CHANGED
|
@@ -648,6 +648,8 @@ export async function POST(req: NextRequest) {
|
|
|
648
648
|
path.join(projectRoot, 'app', 'node_modules', 'pi-mcp-adapter', 'index.ts'),
|
|
649
649
|
// IM extension: 8-platform IM integration (Telegram, Feishu, Discord, Slack, etc.)
|
|
650
650
|
path.join(projectRoot, 'app', 'lib', 'im', 'index.ts'),
|
|
651
|
+
// pi-subagents: task delegation to subagents with chains, parallel, async support
|
|
652
|
+
path.join(projectRoot, 'app', 'node_modules', 'pi-subagents', 'index.ts'),
|
|
651
653
|
],
|
|
652
654
|
});
|
|
653
655
|
await resourceLoader.reload();
|
|
@@ -35,6 +35,7 @@ export async function PUT(req: NextRequest) {
|
|
|
35
35
|
credentials?: Record<string, string>;
|
|
36
36
|
conversation?: {
|
|
37
37
|
enabled?: boolean;
|
|
38
|
+
transport?: 'webhook' | 'long_connection';
|
|
38
39
|
encrypt_key?: string;
|
|
39
40
|
verification_token?: string;
|
|
40
41
|
public_base_url?: string;
|
|
@@ -69,6 +70,7 @@ export async function PUT(req: NextRequest) {
|
|
|
69
70
|
merged.conversation = {
|
|
70
71
|
...(merged.conversation ?? {}),
|
|
71
72
|
enabled: Boolean(conversation.enabled),
|
|
73
|
+
transport: conversation.transport ?? merged.conversation?.transport ?? 'webhook',
|
|
72
74
|
encrypt_key: conversation.encrypt_key ?? merged.conversation?.encrypt_key,
|
|
73
75
|
verification_token: conversation.verification_token ?? merged.conversation?.verification_token,
|
|
74
76
|
public_base_url: conversation.public_base_url ?? merged.conversation?.public_base_url,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { getPlatformConfig } from '@/lib/im/config';
|
|
3
|
-
import {
|
|
3
|
+
import { dispatchFeishuWebhook } from '@/lib/im/feishu-dispatcher';
|
|
4
4
|
|
|
5
5
|
export async function POST(req: NextRequest) {
|
|
6
6
|
try {
|
|
@@ -10,9 +10,11 @@ export async function POST(req: NextRequest) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const body = await req.json();
|
|
13
|
-
const
|
|
13
|
+
const headers = Object.fromEntries(req.headers.entries());
|
|
14
|
+
const result = await dispatchFeishuWebhook({
|
|
14
15
|
config,
|
|
15
16
|
body,
|
|
17
|
+
headers,
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
return NextResponse.json(result.body, { status: result.status });
|
|
@@ -29,7 +29,7 @@ const VARIANT_ICON = {
|
|
|
29
29
|
upload: { icon: Paperclip, cls: 'text-muted-foreground' },
|
|
30
30
|
image: { icon: ImageIcon, cls: 'text-muted-foreground' },
|
|
31
31
|
skill: { icon: Zap, cls: 'text-[var(--amber)]' },
|
|
32
|
-
agent: { icon: Bot, cls: 'text-
|
|
32
|
+
agent: { icon: Bot, cls: 'text-[var(--amber)]' },
|
|
33
33
|
} as const;
|
|
34
34
|
|
|
35
35
|
const VARIANT_STYLE = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useRef, useEffect, memo, useState, useCallback } from 'react';
|
|
4
|
-
import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap, Copy, Check, ArrowDown, FolderInput, Search, PenLine, Lightbulb, FileText, Paperclip } from 'lucide-react';
|
|
4
|
+
import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap, Copy, Check, ArrowDown, FolderInput, Search, PenLine, Lightbulb, FileText, Paperclip, Bot } from 'lucide-react';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
7
|
import type { Message, ImagePart } from '@/lib/types';
|
|
@@ -111,7 +111,7 @@ function AssistantAgentBadge({ agentName }: { agentName?: string }) {
|
|
|
111
111
|
if (!agentName) return null;
|
|
112
112
|
return (
|
|
113
113
|
<div className="mb-2 inline-flex items-center gap-1 rounded-full border border-[var(--amber)]/15 bg-[var(--amber)]/8 px-2 py-0.5 text-[10px] font-medium tracking-wide text-[var(--amber)]">
|
|
114
|
-
<
|
|
114
|
+
<Bot size={10} className="shrink-0" />
|
|
115
115
|
<span>{agentName}</span>
|
|
116
116
|
</div>
|
|
117
117
|
);
|
|
@@ -25,7 +25,7 @@ export default function CustomProviderFields({
|
|
|
25
25
|
|
|
26
26
|
const nameLabel = locale === 'zh' ? '名称' : 'Name';
|
|
27
27
|
const protocolLabel = locale === 'zh' ? '协议' : 'Protocol';
|
|
28
|
-
const namePlaceholder = locale === 'zh' ? '
|
|
28
|
+
const namePlaceholder = locale === 'zh' ? '可选,默认使用协议名称' : 'Optional, defaults to protocol name';
|
|
29
29
|
|
|
30
30
|
const nameHint = form.isDuplicateName
|
|
31
31
|
? (locale === 'zh' ? '名称已存在' : 'Name already exists')
|
|
@@ -119,6 +119,9 @@ export default function CustomProvidersCard({
|
|
|
119
119
|
onClose={() => setIsModalOpen(false)}
|
|
120
120
|
onSave={handleSaveProvider}
|
|
121
121
|
initialProvider={editingProvider ?? undefined}
|
|
122
|
+
existingNames={providers
|
|
123
|
+
.filter((p) => p.id !== editingId)
|
|
124
|
+
.map((p) => p.name)}
|
|
122
125
|
t={t}
|
|
123
126
|
/>
|
|
124
127
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback } from 'react';
|
|
4
|
-
import { type ProviderId } from '@/lib/agent/providers';
|
|
3
|
+
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
4
|
+
import { type ProviderId, PROVIDER_PRESETS } from '@/lib/agent/providers';
|
|
5
5
|
import { type Provider, generateProviderId } from '@/lib/custom-endpoints';
|
|
6
6
|
|
|
7
7
|
export type TestState = 'idle' | 'testing' | 'ok' | 'error';
|
|
@@ -32,6 +32,29 @@ export interface CustomProviderFormState {
|
|
|
32
32
|
handleSave: () => void;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function buildDefaultProviderName(
|
|
36
|
+
protocol: ProviderId,
|
|
37
|
+
existingNames: string[] = [],
|
|
38
|
+
excludeName?: string,
|
|
39
|
+
locale?: string,
|
|
40
|
+
): string {
|
|
41
|
+
const preset = PROVIDER_PRESETS[protocol];
|
|
42
|
+
const baseName = locale === 'zh' ? preset.nameZh : preset.name;
|
|
43
|
+
const normalizedExisting = new Set(
|
|
44
|
+
existingNames
|
|
45
|
+
.filter((name) => name && name !== excludeName)
|
|
46
|
+
.map((name) => name.trim().toLowerCase()),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (!normalizedExisting.has(baseName.trim().toLowerCase())) return baseName;
|
|
50
|
+
|
|
51
|
+
let index = 2;
|
|
52
|
+
while (normalizedExisting.has(`${baseName} ${index}`.toLowerCase())) {
|
|
53
|
+
index++;
|
|
54
|
+
}
|
|
55
|
+
return `${baseName} ${index}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
/**
|
|
36
59
|
* Shared form state + test/save logic for provider forms.
|
|
37
60
|
* Used by both the inline form (AiTab) and the modal (ProviderModal).
|
|
@@ -47,27 +70,64 @@ export function useCustomProviderForm({
|
|
|
47
70
|
locale: string;
|
|
48
71
|
existingNames?: string[];
|
|
49
72
|
}): CustomProviderFormState {
|
|
50
|
-
const
|
|
51
|
-
|
|
73
|
+
const initialName = useMemo(
|
|
74
|
+
() => initial?.name ?? buildDefaultProviderName(initial?.protocol ?? 'openai', existingNames, initial?.name, locale),
|
|
75
|
+
[initial?.name, initial?.protocol, existingNames, locale],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const [name, setNameState] = useState(initialName);
|
|
79
|
+
const [protocol, setProtocolState] = useState<ProviderId>(initial?.protocol ?? 'openai');
|
|
52
80
|
const [apiKey, setApiKey] = useState(initial?.apiKey ?? '');
|
|
53
81
|
const [model, setModel] = useState(initial?.model ?? '');
|
|
54
82
|
const [baseUrl, setBaseUrl] = useState(initial?.baseUrl ?? '');
|
|
55
83
|
const [testResult, setTestResult] = useState<TestResult>({ state: 'idle' });
|
|
84
|
+
const [nameTouched, setNameTouched] = useState(!!initial?.name);
|
|
85
|
+
|
|
86
|
+
const autoName = useMemo(
|
|
87
|
+
() => buildDefaultProviderName(protocol, existingNames, initial?.name, locale),
|
|
88
|
+
[protocol, existingNames, initial?.name, locale],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!nameTouched) {
|
|
93
|
+
setNameState(autoName);
|
|
94
|
+
}
|
|
95
|
+
}, [autoName, nameTouched]);
|
|
96
|
+
|
|
97
|
+
const setName = useCallback((value: string) => {
|
|
98
|
+
setNameTouched(true);
|
|
99
|
+
setNameState(value);
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
const setProtocol = useCallback((value: ProviderId) => {
|
|
103
|
+
setProtocolState(value);
|
|
104
|
+
setTestResult({ state: 'idle' });
|
|
105
|
+
}, []);
|
|
56
106
|
|
|
57
107
|
// Check for duplicate name (exclude the provider being edited)
|
|
58
108
|
const trimmedName = name.trim();
|
|
59
|
-
const
|
|
60
|
-
|
|
109
|
+
const effectiveName = trimmedName || autoName;
|
|
110
|
+
const isDuplicateName = !!(effectiveName && existingNames?.some(
|
|
111
|
+
n => n !== initial?.name && n.toLowerCase() === effectiveName.toLowerCase(),
|
|
61
112
|
));
|
|
62
113
|
|
|
63
|
-
const canSave = !!(
|
|
114
|
+
const canSave = !!(baseUrl.trim() && model.trim() && !isDuplicateName);
|
|
115
|
+
|
|
116
|
+
const requiredFieldsMessage = locale === 'zh'
|
|
117
|
+
? '接口地址和模型为必填'
|
|
118
|
+
: 'Base URL and model are required';
|
|
119
|
+
|
|
120
|
+
const duplicateNameMessage = locale === 'zh'
|
|
121
|
+
? '名称已存在,请使用其他名称'
|
|
122
|
+
: 'Name already exists, please use a different name';
|
|
64
123
|
|
|
65
124
|
const handleTest = useCallback(async () => {
|
|
66
|
-
if (
|
|
67
|
-
setTestResult({
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
125
|
+
if (isDuplicateName) {
|
|
126
|
+
setTestResult({ state: 'error', error: duplicateNameMessage });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!baseUrl.trim() || !model.trim()) {
|
|
130
|
+
setTestResult({ state: 'error', error: requiredFieldsMessage });
|
|
71
131
|
return;
|
|
72
132
|
}
|
|
73
133
|
setTestResult({ state: 'testing' });
|
|
@@ -90,32 +150,26 @@ export function useCustomProviderForm({
|
|
|
90
150
|
} catch {
|
|
91
151
|
setTestResult({ state: 'error', code: 'network_error', error: 'Network error' });
|
|
92
152
|
}
|
|
93
|
-
}, [
|
|
153
|
+
}, [isDuplicateName, duplicateNameMessage, baseUrl, model, initial?.id, apiKey, protocol, requiredFieldsMessage]);
|
|
94
154
|
|
|
95
155
|
const handleSave = useCallback(() => {
|
|
96
156
|
if (isDuplicateName) {
|
|
97
|
-
setTestResult({
|
|
98
|
-
state: 'error',
|
|
99
|
-
error: locale === 'zh' ? '名称已存在,请使用其他名称' : 'Name already exists, please use a different name',
|
|
100
|
-
});
|
|
157
|
+
setTestResult({ state: 'error', error: duplicateNameMessage });
|
|
101
158
|
return;
|
|
102
159
|
}
|
|
103
|
-
if (!
|
|
104
|
-
setTestResult({
|
|
105
|
-
state: 'error',
|
|
106
|
-
error: locale === 'zh' ? '名称、接口地址和模型为必填' : 'Name, base URL, and model are required',
|
|
107
|
-
});
|
|
160
|
+
if (!baseUrl.trim() || !model.trim()) {
|
|
161
|
+
setTestResult({ state: 'error', error: requiredFieldsMessage });
|
|
108
162
|
return;
|
|
109
163
|
}
|
|
110
164
|
onSave({
|
|
111
165
|
id: initial?.id || generateProviderId(),
|
|
112
|
-
name:
|
|
166
|
+
name: effectiveName,
|
|
113
167
|
protocol,
|
|
114
168
|
apiKey,
|
|
115
169
|
model: model.trim(),
|
|
116
170
|
baseUrl: baseUrl.trim(),
|
|
117
171
|
});
|
|
118
|
-
}, [
|
|
172
|
+
}, [isDuplicateName, duplicateNameMessage, baseUrl, model, requiredFieldsMessage, onSave, initial?.id, effectiveName, protocol, apiKey]);
|
|
119
173
|
|
|
120
174
|
return {
|
|
121
175
|
name, setName,
|
|
@@ -39,7 +39,8 @@ export default function ProviderSelect({
|
|
|
39
39
|
: [];
|
|
40
40
|
|
|
41
41
|
// Add panel shows ALL providers as protocol templates (can add multiple of the same type)
|
|
42
|
-
const { primary: primaryItems, more: moreItems } = groups;
|
|
42
|
+
const { primary: primaryItems, local: localItems, more: moreItems } = groups;
|
|
43
|
+
const secondaryItems = [...localItems, ...moreItems];
|
|
43
44
|
|
|
44
45
|
/* ── Compact tab button (for legacy builtin-only mode) ── */
|
|
45
46
|
const renderCompactTab = (id: ProviderId) => {
|
|
@@ -175,7 +176,7 @@ export default function ProviderSelect({
|
|
|
175
176
|
</div>
|
|
176
177
|
|
|
177
178
|
{/* More toggle */}
|
|
178
|
-
{
|
|
179
|
+
{secondaryItems.length > 0 && (
|
|
179
180
|
<>
|
|
180
181
|
<button
|
|
181
182
|
type="button"
|
|
@@ -186,13 +187,13 @@ export default function ProviderSelect({
|
|
|
186
187
|
{showMore
|
|
187
188
|
? (locale === 'zh' ? '收起' : 'Show less')
|
|
188
189
|
: (locale === 'zh'
|
|
189
|
-
? `更多 (${
|
|
190
|
-
: `More (${
|
|
190
|
+
? `更多 (${secondaryItems.length})`
|
|
191
|
+
: `More (${secondaryItems.length})`)}
|
|
191
192
|
</button>
|
|
192
193
|
|
|
193
194
|
{showMore && (
|
|
194
195
|
<div className={compact ? 'flex flex-wrap gap-2' : 'grid grid-cols-1 gap-2'}>
|
|
195
|
-
{
|
|
196
|
+
{secondaryItems.map(id => compact ? renderCompactTab(id) : renderCard(id))}
|
|
196
197
|
</div>
|
|
197
198
|
)}
|
|
198
199
|
</>
|
|
@@ -18,7 +18,7 @@ export type ProviderId =
|
|
|
18
18
|
| 'xai' | 'openrouter' | 'mistral' | 'deepseek'
|
|
19
19
|
| 'zai' | 'zai-cn' | 'kimi-coding'
|
|
20
20
|
| 'cerebras' | 'minimax' | 'minimax-cn' | 'huggingface'
|
|
21
|
-
| 'ollama';
|
|
21
|
+
| 'ollama' | 'lm-studio' | 'vllm';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* UI/UX metadata for each provider.
|
|
@@ -44,7 +44,7 @@ export interface ProviderPreset {
|
|
|
44
44
|
supportsThinking: boolean;
|
|
45
45
|
supportsListModels: boolean;
|
|
46
46
|
signupUrl?: string;
|
|
47
|
-
category: 'primary' | 'more';
|
|
47
|
+
category: 'primary' | 'local' | 'more';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export const PROVIDER_PRESETS: Record<ProviderId, ProviderPreset> = {
|
|
@@ -242,7 +242,41 @@ export const PROVIDER_PRESETS: Record<ProviderId, ProviderPreset> = {
|
|
|
242
242
|
supportsThinking: false,
|
|
243
243
|
supportsListModels: true,
|
|
244
244
|
signupUrl: 'https://ollama.com/download',
|
|
245
|
-
category: '
|
|
245
|
+
category: 'local',
|
|
246
|
+
},
|
|
247
|
+
'lm-studio': {
|
|
248
|
+
id: 'lm-studio',
|
|
249
|
+
name: 'LM Studio',
|
|
250
|
+
nameZh: 'LM Studio (本地)',
|
|
251
|
+
shortLabel: 'LM Studio',
|
|
252
|
+
description: 'Local OpenAI-compatible server',
|
|
253
|
+
descriptionZh: '本地 OpenAI 兼容服务',
|
|
254
|
+
defaultModel: 'local-model',
|
|
255
|
+
piProviderOverride: 'openai' as KnownProvider,
|
|
256
|
+
fixedBaseUrl: 'http://localhost:1234/v1',
|
|
257
|
+
apiKeyFallback: 'lm-studio',
|
|
258
|
+
supportsBaseUrl: true,
|
|
259
|
+
supportsThinking: false,
|
|
260
|
+
supportsListModels: true,
|
|
261
|
+
signupUrl: 'https://lmstudio.ai/',
|
|
262
|
+
category: 'local',
|
|
263
|
+
},
|
|
264
|
+
vllm: {
|
|
265
|
+
id: 'vllm',
|
|
266
|
+
name: 'vLLM',
|
|
267
|
+
nameZh: 'vLLM (本地)',
|
|
268
|
+
shortLabel: 'vLLM',
|
|
269
|
+
description: 'Local OpenAI-compatible server',
|
|
270
|
+
descriptionZh: '本地 OpenAI 兼容服务',
|
|
271
|
+
defaultModel: 'local-model',
|
|
272
|
+
piProviderOverride: 'openai' as KnownProvider,
|
|
273
|
+
fixedBaseUrl: 'http://localhost:8000/v1',
|
|
274
|
+
apiKeyFallback: 'vllm',
|
|
275
|
+
supportsBaseUrl: true,
|
|
276
|
+
supportsThinking: false,
|
|
277
|
+
supportsListModels: true,
|
|
278
|
+
signupUrl: 'https://docs.vllm.ai/',
|
|
279
|
+
category: 'local',
|
|
246
280
|
},
|
|
247
281
|
};
|
|
248
282
|
|
|
@@ -256,15 +290,19 @@ export function getPreset(id: ProviderId): ProviderPreset {
|
|
|
256
290
|
return PROVIDER_PRESETS[id] ?? PROVIDER_PRESETS.anthropic;
|
|
257
291
|
}
|
|
258
292
|
|
|
259
|
-
export function groupedProviders(): { primary: ProviderId[]; more: ProviderId[] } {
|
|
293
|
+
export function groupedProviders(): { primary: ProviderId[]; local: ProviderId[]; more: ProviderId[] } {
|
|
260
294
|
const primary: ProviderId[] = [];
|
|
295
|
+
const local: ProviderId[] = [];
|
|
261
296
|
const more: ProviderId[] = [];
|
|
262
297
|
for (const id of ALL_PROVIDER_IDS) {
|
|
263
|
-
|
|
298
|
+
const category = PROVIDER_PRESETS[id].category;
|
|
299
|
+
if (category === 'primary') primary.push(id);
|
|
300
|
+
else if (category === 'local') local.push(id);
|
|
264
301
|
else more.push(id);
|
|
265
302
|
}
|
|
303
|
+
local.sort((a, b) => PROVIDER_PRESETS[a].name.localeCompare(PROVIDER_PRESETS[b].name));
|
|
266
304
|
more.sort((a, b) => PROVIDER_PRESETS[a].name.localeCompare(PROVIDER_PRESETS[b].name));
|
|
267
|
-
return { primary, more };
|
|
305
|
+
return { primary, local, more };
|
|
268
306
|
}
|
|
269
307
|
|
|
270
308
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { FeishuConfig, FeishuWebhookDispatchResult } from './types';
|
|
2
|
+
import { buildFeishuWebhookStatus, handleFeishuMessageReceiveEvent } from './webhook/feishu';
|
|
3
|
+
|
|
4
|
+
type FeishuHeaders = Record<string, string>;
|
|
5
|
+
type FeishuBody = Record<string, unknown>;
|
|
6
|
+
type FeishuDispatcher = {
|
|
7
|
+
encryptKey?: string;
|
|
8
|
+
register(handles: Record<string, (data: unknown) => unknown>): FeishuDispatcher;
|
|
9
|
+
invoke(data: unknown, params?: { needCheck?: boolean }): Promise<unknown>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type LarkSdkModule = typeof import('@larksuiteoapi/node-sdk');
|
|
13
|
+
|
|
14
|
+
let cachedSdk: LarkSdkModule | null = null;
|
|
15
|
+
let cachedDispatcher: { key: string; dispatcher: FeishuDispatcher } | null = null;
|
|
16
|
+
|
|
17
|
+
function buildDispatcherKey(config: FeishuConfig): string {
|
|
18
|
+
return JSON.stringify({
|
|
19
|
+
encryptKey: config.conversation?.encrypt_key ?? '',
|
|
20
|
+
verificationToken: config.conversation?.verification_token ?? '',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildPayload(body: FeishuBody, headers: FeishuHeaders): FeishuBody {
|
|
25
|
+
return Object.assign(Object.create({ headers }), body);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeDispatcherBody(result: unknown): Record<string, unknown> {
|
|
29
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
30
|
+
return result as Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
if (result == null) {
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
return { ok: false, error: `Unexpected Feishu dispatcher result: ${String(result)}` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getLarkSdk(): Promise<LarkSdkModule> {
|
|
39
|
+
if (cachedSdk) return cachedSdk;
|
|
40
|
+
cachedSdk = await import('@larksuiteoapi/node-sdk');
|
|
41
|
+
return cachedSdk;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function getDispatcher(config: FeishuConfig): Promise<FeishuDispatcher> {
|
|
45
|
+
const key = buildDispatcherKey(config);
|
|
46
|
+
if (cachedDispatcher?.key === key) {
|
|
47
|
+
return cachedDispatcher.dispatcher;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const lark = await getLarkSdk();
|
|
51
|
+
const dispatcher = new lark.EventDispatcher({
|
|
52
|
+
encryptKey: config.conversation?.encrypt_key,
|
|
53
|
+
verificationToken: config.conversation?.verification_token,
|
|
54
|
+
}).register({
|
|
55
|
+
'im.message.receive_v1': (event: unknown) => handleFeishuMessageReceiveEvent(event as import('./types').FeishuSdkMessageEvent),
|
|
56
|
+
}) as FeishuDispatcher;
|
|
57
|
+
|
|
58
|
+
cachedDispatcher = { key, dispatcher };
|
|
59
|
+
return dispatcher;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shouldHandleChallenge(body: FeishuBody): boolean {
|
|
63
|
+
return typeof body.challenge === 'string'
|
|
64
|
+
|| body.type === 'url_verification'
|
|
65
|
+
|| typeof body.encrypt === 'string';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function dispatchFeishuWebhook(params: {
|
|
69
|
+
config: FeishuConfig;
|
|
70
|
+
body: FeishuBody;
|
|
71
|
+
headers: FeishuHeaders;
|
|
72
|
+
}): Promise<FeishuWebhookDispatchResult> {
|
|
73
|
+
const status = buildFeishuWebhookStatus(params.config);
|
|
74
|
+
if (status.state !== 'ready') {
|
|
75
|
+
return {
|
|
76
|
+
status: 202,
|
|
77
|
+
body: { ok: false, ignored: true, reason: status.lastError ?? 'Webhook is not ready' },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const payload = buildPayload(params.body, params.headers);
|
|
82
|
+
if (shouldHandleChallenge(params.body)) {
|
|
83
|
+
const lark = await getLarkSdk();
|
|
84
|
+
const { isChallenge, challenge } = lark.generateChallenge(payload, {
|
|
85
|
+
encryptKey: params.config.conversation?.encrypt_key ?? '',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (isChallenge) {
|
|
89
|
+
return {
|
|
90
|
+
status: 200,
|
|
91
|
+
body: challenge,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const dispatcher = await getDispatcher(params.config);
|
|
97
|
+
const result = await dispatcher.invoke(payload);
|
|
98
|
+
|
|
99
|
+
if (typeof result === 'undefined') {
|
|
100
|
+
return {
|
|
101
|
+
status: 401,
|
|
102
|
+
body: { ok: false, error: 'Invalid Feishu webhook signature or payload.' },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const normalized = normalizeDispatcherBody(result);
|
|
107
|
+
return {
|
|
108
|
+
status: normalized.ok === false ? 500 : 202,
|
|
109
|
+
body: normalized,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as Lark from '@larksuiteoapi/node-sdk';
|
|
2
|
+
import type { FeishuConfig, FeishuSdkMessageEvent } from './types';
|
|
3
|
+
import { handleFeishuMessageReceiveEvent } from './webhook/feishu';
|
|
4
|
+
|
|
5
|
+
type FeishuWSRuntime = {
|
|
6
|
+
client: Lark.WSClient;
|
|
7
|
+
startedAt: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let runtime: FeishuWSRuntime | null = null;
|
|
11
|
+
let lastError: string | undefined;
|
|
12
|
+
|
|
13
|
+
function assertFeishuWSConfig(config: FeishuConfig): void {
|
|
14
|
+
if (!config.app_id?.trim() || !config.app_secret?.trim()) {
|
|
15
|
+
throw new Error('Feishu App ID and App Secret are required for long connection mode');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createDispatcher(): Lark.EventDispatcher {
|
|
20
|
+
return new Lark.EventDispatcher({}).register({
|
|
21
|
+
'im.message.receive_v1': (event: unknown) => handleFeishuMessageReceiveEvent(event as FeishuSdkMessageEvent),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function startFeishuWSClient(config: FeishuConfig): Promise<void> {
|
|
26
|
+
if (runtime) return;
|
|
27
|
+
|
|
28
|
+
assertFeishuWSConfig(config);
|
|
29
|
+
lastError = undefined;
|
|
30
|
+
|
|
31
|
+
const client = new Lark.WSClient({
|
|
32
|
+
appId: config.app_id,
|
|
33
|
+
appSecret: config.app_secret,
|
|
34
|
+
autoReconnect: true,
|
|
35
|
+
loggerLevel: Lark.LoggerLevel.info,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await client.start({
|
|
40
|
+
eventDispatcher: createDispatcher(),
|
|
41
|
+
});
|
|
42
|
+
runtime = {
|
|
43
|
+
client,
|
|
44
|
+
startedAt: new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function stopFeishuWSClient(): void {
|
|
53
|
+
runtime?.client.close();
|
|
54
|
+
runtime = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getFeishuWSClientStatus(): {
|
|
58
|
+
running: boolean;
|
|
59
|
+
startedAt?: string;
|
|
60
|
+
lastError?: string;
|
|
61
|
+
} {
|
|
62
|
+
return {
|
|
63
|
+
running: runtime !== null,
|
|
64
|
+
startedAt: runtime?.startedAt,
|
|
65
|
+
lastError,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function __resetFeishuWSClientForTests(): void {
|
|
70
|
+
runtime = null;
|
|
71
|
+
lastError = undefined;
|
|
72
|
+
}
|